fix: add request tags and desc for glossary and terms (#14071)

* fix: add request tags and desc for glossary and terms

* fix: breadcrumbs

* fix: add request tags cypress
This commit is contained in:
karanh37 2023-11-23 18:46:46 +05:30 committed by GitHub
parent 631145d090
commit 312b038b2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 235 additions and 31 deletions

View File

@ -39,10 +39,12 @@ import {
SEARCH_ENTITY_TABLE,
} from '../../constants/constants';
const name = `test_dataconsumer${uuid()}`;
const CREDENTIALS = {
firstName: 'Cypress',
lastName: 'UserDC',
email: `test_dataconsumer${uuid()}@openmetadata.org`,
email: `${name}@openmetadata.org`,
password: 'User@OMD123',
username: 'CypressUserDC',
};
@ -278,9 +280,7 @@ const updateTags = (inTerm) => {
'/api/v1/search/query?q=*&index=tag_search_index&from=0&size=*&query_filter=*',
'tags'
);
cy.get(
'[data-testid="tags-input-container"] [data-testid="add-tag"]'
).click();
cy.get('[data-testid="tags-container"] [data-testid="add-tag"]').click();
verifyResponseStatusCode('@tags', 200);
@ -294,7 +294,7 @@ const updateTags = (inTerm) => {
cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click();
const container = inTerm
? '[data-testid="tags-input-container"]'
? '[data-testid="tags-container"]'
: '[data-testid="glossary-details"]';
cy.wait(1000);
cy.get(container).scrollIntoView().contains('Personal').should('be.visible');
@ -636,7 +636,7 @@ describe('Glossary page should work properly', () => {
// Remove Tag
cy.get(
'[data-testid="tags-input-container"] [data-testid="edit-button"]'
'[data-testid="tags-container"] [data-testid="edit-button"]'
).click();
cy.get('[data-testid="remove-tags"]').should('be.visible').click();
@ -768,6 +768,64 @@ describe('Glossary page should work properly', () => {
voteGlossary();
});
it('Request Tags workflow for Glossary', function () {
cy.get('[data-testid="glossary-left-panel"]')
.contains(NEW_GLOSSARY_1.name)
.click();
interceptURL(
'GET',
`/api/v1/search/query?q=*%20AND%20disabled:false&index=tag_search_index*`,
'suggestTag'
);
interceptURL('POST', '/api/v1/feed', 'taskCreated');
interceptURL('PUT', '/api/v1/feed/tasks/*/resolve', 'taskResolve');
cy.get('[data-testid="request-entity-tags"]').should('exist').click();
// set assignees for task
cy.get(
'[data-testid="select-assignee"] > .ant-select-selector > .ant-select-selection-overflow'
)
.click()
.type(name);
cy.get(`[data-testid="assignee-option-${name}"]`).click();
cy.clickOutside();
cy.get('[data-testid="tag-selector"]')
.click()
.type('{backspace}')
.type('{backspace}')
.type('Personal');
verifyResponseStatusCode('@suggestTag', 200);
cy.get(
'.ant-select-dropdown [data-testid="tag-PersonalData.Personal"]'
).click();
cy.clickOutside();
cy.get('[data-testid="submit-tag-request"]').click();
verifyResponseStatusCode('@taskCreated', 201);
// Accept the tag suggestion which is created
cy.get('.ant-btn-compact-first-item').contains('Accept Suggestion').click();
verifyResponseStatusCode('@taskResolve', 200);
cy.reload();
cy.get('[data-testid="glossary-left-panel"]')
.contains(NEW_GLOSSARY_1.name)
.click();
checkDisplayName(NEW_GLOSSARY_1.name);
// Verify Tags which is added at the time of creating glossary
cy.get('[data-testid="tags-container"]')
.contains('Personal')
.should('be.visible');
});
it('Assets Tab should work properly', () => {
selectActiveGlossary(NEW_GLOSSARY.name);
const glossary = NEW_GLOSSARY.name;

View File

@ -46,6 +46,7 @@ const GlossaryDetails = ({
onAddGlossaryTerm,
onEditGlossaryTerm,
isVersionView,
onThreadLinkSelect,
}: GlossaryDetailsProps) => {
const { t } = useTranslation();
const history = useHistory();
@ -131,15 +132,19 @@ const GlossaryDetails = ({
<Space className="w-full" direction="vertical" size={24}>
<DescriptionV1
description={description}
entityFqn={glossary.fullyQualifiedName}
entityName={glossary.displayName ?? glossary.name}
entityType={EntityType.GLOSSARY}
hasEditAccess={permissions.EditDescription || permissions.EditAll}
isEdit={isDescriptionEditable}
showCommentsIcon={false}
owner={glossary?.owner}
showActions={!glossary.deleted}
onCancel={() => setIsDescriptionEditable(false)}
onDescriptionEdit={() => setIsDescriptionEditable(true)}
onDescriptionUpdate={onDescriptionUpdate}
onThreadLinkSelect={onThreadLinkSelect}
/>
<GlossaryTermTab
isGlossary
childGlossaryTerms={glossaryTerms}
@ -158,6 +163,7 @@ const GlossaryDetails = ({
isVersionView={isVersionView}
permissions={permissions}
selectedData={glossary}
onThreadLinkSelect={onThreadLinkSelect}
onUpdate={updateGlossary}
/>
</Col>

View File

@ -33,4 +33,5 @@ export type GlossaryDetailsProps = {
refreshGlossaryTerms: () => void;
onAddGlossaryTerm: (glossaryTerm: GlossaryTerm | undefined) => void;
onEditGlossaryTerm: (glossaryTerm: GlossaryTerm) => void;
onThreadLinkSelect: (value: string) => void;
};

View File

@ -65,6 +65,7 @@ const mockProps = {
onAddGlossaryTerm: jest.fn(),
onEditGlossaryTerm: jest.fn(),
updateVote: jest.fn(),
onThreadLinkSelect: jest.fn(),
};
describe('Test Glossary-details component', () => {

View File

@ -22,7 +22,6 @@ import { UserSelectableList } from '../../../components/common/UserSelectableLis
import { UserTeamSelectableList } from '../../../components/common/UserTeamSelectableList/UserTeamSelectableList.component';
import { OperationPermission } from '../../../components/PermissionProvider/PermissionProvider.interface';
import TagButton from '../../../components/TagButton/TagButton.component';
import TagsInput from '../../../components/TagsInput/TagsInput.component';
import {
DE_ACTIVE_COLOR,
getTeamAndUserDetailsPath,
@ -31,7 +30,7 @@ import {
} from '../../../constants/constants';
import { EntityField } from '../../../constants/Feeds.constants';
import { EntityType } from '../../../enums/entity.enum';
import { Glossary } from '../../../generated/entity/data/glossary';
import { Glossary, TagSource } from '../../../generated/entity/data/glossary';
import {
GlossaryTerm,
TagLabel,
@ -47,6 +46,8 @@ import {
getEntityVersionTags,
} from '../../../utils/EntityVersionUtils';
import { DomainLabel } from '../../common/DomainLabel/DomainLabel.component';
import TagsContainerV2 from '../../Tag/TagsContainerV2/TagsContainerV2';
import { DisplayType } from '../../Tag/TagsViewer/TagsViewer.interface';
import GlossaryReviewers from './GlossaryReviewers';
type Props = {
@ -55,6 +56,7 @@ type Props = {
selectedData: Glossary | GlossaryTerm;
isGlossary: boolean;
onUpdate: (data: GlossaryTerm | Glossary) => void;
onThreadLinkSelect: (value: string) => void;
};
const GlossaryDetailsRightPanel = ({
@ -63,6 +65,7 @@ const GlossaryDetailsRightPanel = ({
isGlossary,
onUpdate,
isVersionView,
onThreadLinkSelect,
}: Props) => {
const hasEditReviewerAccess = useMemo(() => {
return permissions.EditAll || permissions.EditReviewers;
@ -307,11 +310,15 @@ const GlossaryDetailsRightPanel = ({
<Col span="24">
<div data-testid="glossary-tags-name">
{isGlossary && (
<TagsInput
editable={permissions.EditAll || permissions.EditTags}
isVersionView={isVersionView}
tags={tags}
onTagsUpdate={handleTagsUpdate}
<TagsContainerV2
displayType={DisplayType.READ_MORE}
entityFqn={selectedData.fullyQualifiedName}
entityType={EntityType.GLOSSARY}
permission={permissions.EditAll || permissions.EditTags}
selectedTags={tags ?? []}
tagType={TagSource.Classification}
onSelectionChange={handleTagsUpdate}
onThreadLinkSelect={onThreadLinkSelect}
/>
)}
</div>

View File

@ -48,6 +48,7 @@ describe('GlossaryDetailsRightPanel', () => {
isGlossary
permissions={mockPermissions}
selectedData={mockedGlossaries[0]}
onThreadLinkSelect={jest.fn()}
onUpdate={jest.fn()}
/>
</BrowserRouter>

View File

@ -63,6 +63,7 @@ const GlossaryTermsV1 = ({
updateVote,
refreshActiveGlossaryTerm,
isVersionView,
onThreadLinkSelect,
}: GlossaryTermsV1Props) => {
const {
fqn: glossaryFqn,
@ -130,6 +131,7 @@ const GlossaryTermsV1 = ({
isVersionView={isVersionView}
permissions={permissions}
selectedData={glossaryTerm}
onThreadLinkSelect={onThreadLinkSelect}
onUpdate={(data) => handleGlossaryTermUpdate(data as GlossaryTerm)}
/>
),

View File

@ -30,4 +30,5 @@ export interface GlossaryTermsV1Props {
onEditGlossaryTerm: (glossaryTerm: GlossaryTerm) => void;
updateVote?: (data: VotingDataProps) => Promise<void>;
refreshActiveGlossaryTerm?: () => void;
onThreadLinkSelect: (value: string) => void;
}

View File

@ -83,6 +83,7 @@ const mockProps = {
refreshActiveGlossaryTerm: jest.fn(),
onAddGlossaryTerm: jest.fn(),
onEditGlossaryTerm: jest.fn(),
onThreadLinkSelect: jest.fn(),
};
describe('Test Glossary-term component', () => {

View File

@ -17,7 +17,7 @@ import { EntityType } from '../../../../enums/entity.enum';
import { Glossary } from '../../../../generated/entity/data/glossary';
import { GlossaryTerm } from '../../../../generated/entity/data/glossaryTerm';
import { ChangeDescription } from '../../../../generated/entity/type';
import { TagLabel } from '../../../../generated/type/tagLabel';
import { TagLabel, TagSource } from '../../../../generated/type/tagLabel';
import { getEntityName } from '../../../../utils/EntityUtils';
import {
getEntityVersionByField,
@ -25,7 +25,8 @@ import {
} from '../../../../utils/EntityVersionUtils';
import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1';
import { OperationPermission } from '../../../PermissionProvider/PermissionProvider.interface';
import TagsInput from '../../../TagsInput/TagsInput.component';
import TagsContainerV2 from '../../../Tag/TagsContainerV2/TagsContainerV2';
import { DisplayType } from '../../../Tag/TagsViewer/TagsViewer.interface';
import GlossaryDetailsRightPanel from '../../GlossaryDetailsRightPanel/GlossaryDetailsRightPanel.component';
import GlossaryTermReferences from './GlossaryTermReferences';
import GlossaryTermSynonyms from './GlossaryTermSynonyms';
@ -37,6 +38,7 @@ type Props = {
onUpdate: (data: GlossaryTerm | Glossary) => Promise<void>;
isGlossary: boolean;
isVersionView?: boolean;
onThreadLinkSelect: (value: string) => void;
};
const GlossaryOverviewTab = ({
@ -45,6 +47,7 @@ const GlossaryOverviewTab = ({
onUpdate,
isGlossary,
isVersionView,
onThreadLinkSelect,
}: Props) => {
const [isDescriptionEditable, setIsDescriptionEditable] =
useState<boolean>(false);
@ -110,14 +113,17 @@ const GlossaryOverviewTab = ({
<Col span={24}>
<DescriptionV1
description={glossaryDescription}
entityFqn={selectedData.fullyQualifiedName}
entityName={getEntityName(selectedData)}
entityType={EntityType.GLOSSARY}
entityType={EntityType.GLOSSARY_TERM}
hasEditAccess={permissions.EditDescription || permissions.EditAll}
isEdit={isDescriptionEditable}
showCommentsIcon={false}
owner={selectedData?.owner}
showActions={!selectedData.deleted}
onCancel={() => setIsDescriptionEditable(false)}
onDescriptionEdit={() => setIsDescriptionEditable(true)}
onDescriptionUpdate={onDescriptionUpdate}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Col>
<Col span={24}>
@ -153,11 +159,15 @@ const GlossaryOverviewTab = ({
<Col span={12}>
<Space className="w-full" direction="vertical">
<TagsInput
editable={hasEditTagsPermissions}
isVersionView={isVersionView}
tags={tags}
onTagsUpdate={handleTagsUpdate}
<TagsContainerV2
displayType={DisplayType.READ_MORE}
entityFqn={selectedData.fullyQualifiedName}
entityType={EntityType.GLOSSARY_TERM}
permission={hasEditTagsPermissions}
selectedTags={tags ?? []}
tagType={TagSource.Classification}
onSelectionChange={handleTagsUpdate}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Space>
</Col>
@ -171,6 +181,7 @@ const GlossaryOverviewTab = ({
isVersionView={isVersionView}
permissions={permissions}
selectedData={selectedData}
onThreadLinkSelect={onThreadLinkSelect}
onUpdate={onUpdate}
/>
</Col>

View File

@ -50,6 +50,7 @@ describe('GlossaryOverviewTab', () => {
isGlossary={isGlossary}
permissions={permissions}
selectedData={selectedData}
onThreadLinkSelect={jest.fn()}
onUpdate={onUpdate}
/>
</BrowserRouter>

View File

@ -25,9 +25,14 @@ import {
getGlossaryTermDetailsPath,
} from '../../constants/constants';
import { EntityAction } from '../../enums/entity.enum';
import {
CreateThread,
ThreadType,
} from '../../generated/api/feed/createThread';
import { Glossary } from '../../generated/entity/data/glossary';
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
import { VERSION_VIEW_GLOSSARY_PERMISSION } from '../../mocks/Glossary.mock';
import { postThread } from '../../rest/feedsAPI';
import {
addGlossaryTerm,
getGlossaryTerms,
@ -37,6 +42,8 @@ import {
import { getEntityDeleteMessage } from '../../utils/CommonUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { useActivityFeedProvider } from '../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
import EntityDeleteModal from '../Modals/EntityDeleteModal/EntityDeleteModal';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import {
@ -69,6 +76,11 @@ const GlossaryV1 = ({
const { action, tab } =
useParams<{ action: EntityAction; glossaryName: string; tab: string }>();
const history = useHistory();
const [threadLink, setThreadLink] = useState<string>('');
const [threadType, setThreadType] = useState<ThreadType>(
ThreadType.Conversation
);
const { postFeed, deleteFeed, updateFeed } = useActivityFeedProvider();
const { getEntityPermission } = usePermissionProvider();
const [isLoading, setIsLoading] = useState(true);
@ -96,6 +108,17 @@ const GlossaryV1 = ({
[action]
);
const onThreadPanelClose = () => {
setThreadLink('');
};
const onThreadLinkSelect = (link: string, threadType?: ThreadType) => {
setThreadLink(link);
if (threadType) {
setThreadType(threadType);
}
};
const fetchGlossaryTerm = async (
params?: ListGlossaryTermsParams,
refresh?: boolean
@ -139,6 +162,19 @@ const GlossaryV1 = ({
}
};
const createThread = async (data: CreateThread) => {
try {
await postThread(data);
} catch (error) {
showErrorToast(
error as AxiosError,
t('server.create-entity-error', {
entity: t('label.conversation'),
})
);
}
};
const handleDelete = () => {
const { id } = selectedData;
if (isGlossaryActive) {
@ -329,6 +365,7 @@ const GlossaryV1 = ({
onEditGlossaryTerm={(term) =>
handleGlossaryTermModalAction(true, term)
}
onThreadLinkSelect={onThreadLinkSelect}
/>
) : (
<GlossaryTermsV1
@ -350,6 +387,7 @@ const GlossaryV1 = ({
onEditGlossaryTerm={(term) =>
handleGlossaryTermModalAction(true, term)
}
onThreadLinkSelect={onThreadLinkSelect}
/>
))}
@ -376,6 +414,19 @@ const GlossaryV1 = ({
onSave={handleGlossaryTermSave}
/>
)}
{threadLink ? (
<ActivityThreadPanel
createThread={createThread}
deletePostHandler={deleteFeed}
open={Boolean(threadLink)}
postFeedHandler={postFeed}
threadLink={threadLink}
threadType={threadType}
updateThreadHandler={updateFeed}
onCancel={onThreadPanelClose}
/>
) : null}
</>
);
};

View File

@ -342,6 +342,7 @@ export const TaskTab = ({
</Button>
) : (
<Dropdown.Button
data-testid="edit-accept-task-dropdown"
icon={<DownOutlined />}
menu={{
items: TASK_ACTION_LIST,

View File

@ -16,6 +16,8 @@ import { Dashboard } from '../../generated/entity/data/dashboard';
import { DashboardDataModel } from '../../generated/entity/data/dashboardDataModel';
import { Database } from '../../generated/entity/data/database';
import { DatabaseSchema } from '../../generated/entity/data/databaseSchema';
import { Glossary } from '../../generated/entity/data/glossary';
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
import { Mlmodel } from '../../generated/entity/data/mlmodel';
import { Pipeline } from '../../generated/entity/data/pipeline';
import { SearchIndex } from '../../generated/entity/data/searchIndex';
@ -34,7 +36,9 @@ export type EntityData =
| Database
| DatabaseSchema
| DashboardDataModel
| SearchIndex;
| SearchIndex
| Glossary
| GlossaryTerm;
export interface Option {
label: string;

View File

@ -15,7 +15,10 @@ import { AxiosError } from 'axios';
import { isUndefined, omit } from 'lodash';
import { StatusType } from '../components/common/StatusBadge/StatusBadge.interface';
import { ModifiedGlossaryTerm } from '../components/Glossary/GlossaryTermTab/GlossaryTermTab.interface';
import { WILD_CARD_CHAR } from '../constants/char.constants';
import {
FQN_SEPARATOR_CHAR,
WILD_CARD_CHAR,
} from '../constants/char.constants';
import { SearchIndex } from '../enums/search.enum';
import { Glossary } from '../generated/entity/data/glossary';
import { GlossaryTerm, Status } from '../generated/entity/data/glossaryTerm';
@ -24,6 +27,8 @@ import { SearchResponse } from '../interface/search.interface';
import { ListGlossaryTermsParams } from '../rest/glossaryAPI';
import { searchData } from '../rest/miscAPI';
import { formatSearchGlossaryTermResponse } from './APIUtils';
import Fqn from './Fqn';
import { getGlossaryPath } from './RouterUtils';
export interface GlossaryTermTreeNode {
children?: GlossaryTermTreeNode[];
@ -234,3 +239,26 @@ export const StatusFilters = Object.values(Status)
text: status,
value: status,
}));
export const getGlossaryBreadcrumbs = (fqn: string) => {
const arr = Fqn.split(fqn);
const dataFQN: Array<string> = [];
const breadcrumbList = [
{
name: 'Glossaries',
url: getGlossaryPath(''),
activeTitle: false,
},
...arr.map((d) => {
dataFQN.push(d);
return {
name: d,
url: getGlossaryPath(dataFQN.join(FQN_SEPARATOR_CHAR)),
activeTitle: false,
};
}),
];
return breadcrumbList;
};

View File

@ -53,6 +53,7 @@ import {
getDatabaseSchemaDetailsByFQN,
} from '../rest/databaseAPI';
import { getDataModelDetailsByFQN } from '../rest/dataModelsAPI';
import { getGlossariesByName, getGlossaryTermByFQN } from '../rest/glossaryAPI';
import { getUserSuggestions } from '../rest/miscAPI';
import { getMlModelByFQN } from '../rest/mlModelAPI';
import { getPipelineByFqn } from '../rest/pipelineAPI';
@ -73,11 +74,12 @@ import { defaultFields as DataModelFields } from './DataModelsUtils';
import { defaultFields as TableFields } from './DatasetDetailsUtils';
import { getEntityName } from './EntityUtils';
import { getEntityFQN, getEntityType } from './FeedUtils';
import { getGlossaryBreadcrumbs } from './GlossaryUtils';
import { defaultFields as MlModelFields } from './MlModelDetailsUtils';
import { defaultFields as PipelineFields } from './PipelineDetailsUtils';
import serviceUtilClassBase from './ServiceUtilClassBase';
import { STORED_PROCEDURE_DEFAULT_FIELDS } from './StoredProceduresUtils';
import { getEncodedFqn } from './StringsUtils';
import { getDecodedFqn, getEncodedFqn } from './StringsUtils';
import { getEntityLink } from './TableUtils';
import { showErrorToast } from './ToastUtils';
@ -275,6 +277,8 @@ export const TASK_ENTITIES = [
EntityType.DASHBOARD_DATA_MODEL,
EntityType.STORED_PROCEDURE,
EntityType.SEARCH_INDEX,
EntityType.GLOSSARY,
EntityType.GLOSSARY_TERM,
];
export const getBreadCrumbList = (
@ -308,12 +312,17 @@ export const getBreadCrumbList = (
const service = (serviceCategory: ServiceCategory) => {
return {
name: getEntityName(entityData.service),
url: getEntityName(entityData.service)
? getServiceDetailsPath(entityData.service?.name || '', serviceCategory)
name: getEntityName((entityData as Table).service),
url: getEntityName((entityData as Table).service)
? getServiceDetailsPath(
(entityData as Table).service?.name ?? '',
serviceCategory
)
: '',
imgSrc: entityData.serviceType
? serviceUtilClassBase.getServiceTypeLogo(entityData.serviceType)
imgSrc: (entityData as Table).serviceType
? serviceUtilClassBase.getServiceTypeLogo(
(entityData as Table).serviceType as string
)
: undefined,
};
};
@ -372,6 +381,11 @@ export const getBreadCrumbList = (
];
}
case EntityType.GLOSSARY:
case EntityType.GLOSSARY_TERM: {
return getGlossaryBreadcrumbs(entityData.fullyQualifiedName ?? '');
}
default:
return [];
}
@ -485,6 +499,22 @@ export const fetchEntityDetail = (
.catch((err: AxiosError) => showErrorToast(err));
break;
case EntityType.GLOSSARY:
getGlossariesByName(entityFQN, TabSpecificField.TAGS)
.then((res) => {
setEntityData(res);
})
.catch((err: AxiosError) => showErrorToast(err));
break;
case EntityType.GLOSSARY_TERM:
getGlossaryTermByFQN(getDecodedFqn(entityFQN), TabSpecificField.TAGS)
.then((res) => {
setEntityData(res);
})
.catch((err: AxiosError) => showErrorToast(err));
break;
default:
break;