ui: facet filter and tags-container overflow bug fixes (#11119)

* Changed facet filter buckets render length. Only top 10 most used filters will be shown in the left panel.
Fixed the overflow issue of tags-container component for longer tag name

* Fixed overflowing issue while adding tags
Improved Tags component and use antd tag to show tags

* Reduced the gap

* Fixed tooltip placement issue with facet filters

* Fixed breadcrumb issue on dataInsights and ESReIndex pipeline logs page

* Fixed cypress tests

* Added delay for tooltip popup to avoid cypress flakyness

* Added delays to the tooltips and changed positions to avoid flakyness in cypress

* Fixed failing cypress tests

---------

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
Aniket Katkar 2023-04-21 19:01:30 +05:30 committed by GitHub
parent e494cdfb65
commit 7fb30b9548
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 338 additions and 323 deletions

View File

@ -316,10 +316,7 @@ export const addTier = (tier) => {
.should('be.visible') .should('be.visible')
.click(); .click();
cy.get('[data-testid="tags"] > [data-testid="add-tag"]').should( cy.get('[data-testid="tier-dropdown"]').should('contain', 'Tier1');
'contain',
'Tier1'
);
}; };
export const addTag = (tag) => { export const addTag = (tag) => {
@ -329,7 +326,9 @@ export const addTag = (tag) => {
SEARCH_ENTITY_TABLE.table_3.entity SEARCH_ENTITY_TABLE.table_3.entity
); );
cy.get('[data-testid="tags"] > [data-testid="add-tag"]') cy.get(
'[data-testid="entity-tags"] [data-testid="tags"] [data-testid="add-tag"]'
)
.eq(0) .eq(0)
.should('be.visible') .should('be.visible')
.scrollIntoView() .scrollIntoView()

View File

@ -550,7 +550,7 @@ export const addNewTagToEntity = (entityObj, term) => {
entityObj.entity entityObj.entity
); );
cy.wait(500); cy.wait(500);
cy.get('[data-testid="tags"] > [data-testid="add-tag"]') cy.get('[data-testid="tags"] [data-testid="add-tag"]')
.eq(0) .eq(0)
.should('be.visible') .should('be.visible')
.scrollIntoView() .scrollIntoView()

View File

@ -43,9 +43,7 @@ const removeTags = (tag, checkForParentEntity, isTable) => {
.should('be.visible') .should('be.visible')
.click(); .click();
cy.get('.ant-select-selection-item-remove > .anticon') cy.get('[data-testid="remove-tags"]').should('be.visible').click();
.should('be.visible')
.click();
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click(); cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
} else { } else {
@ -65,7 +63,7 @@ const removeTags = (tag, checkForParentEntity, isTable) => {
.click(); .click();
} }
cy.get(`[title="${tag}"] [data-testid="remove-tags"`) cy.get(`[data-testid="selected-tag-${tag}"] [data-testid="remove-tags"`)
.should('be.visible') .should('be.visible')
.click(); .click();
@ -88,7 +86,7 @@ describe('Check if tags addition and removal flow working properly from tables',
); );
cy.get( cy.get(
'[data-testid="entity-tags"] [data-testid="tags-wrapper"] [data-testid="tag-container"] [data-testid="tags"] [data-testid="add-tag"] span' '[data-testid="entity-tags"] [data-testid="tags-wrapper"] [data-testid="tag-container"] [data-testid="tags"] [data-testid="add-tag"]'
) )
.should('be.visible') .should('be.visible')
.click(); .click();
@ -107,13 +105,13 @@ describe('Check if tags addition and removal flow working properly from tables',
if (entityDetails.entity === 'mlmodels') { if (entityDetails.entity === 'mlmodels') {
cy.get( cy.get(
`[data-testid="feature-card-${entityDetails.fieldName}"] [data-testid="tag-container"] [data-testid="tags"] > [data-testid="add-tag"] span` `[data-testid="feature-card-${entityDetails.fieldName}"] [data-testid="tag-container"] [data-testid="tags"] [data-testid="add-tag"]`
) )
.should('be.visible') .should('be.visible')
.click(); .click();
} else { } else {
cy.get( cy.get(
`.ant-table-tbody [data-testid="tag-container"] [data-testid="add-tag"] span` `.ant-table-tbody [data-testid="tag-container"] [data-testid="add-tag"]`
) )
.eq(0) .eq(0)
.should('be.visible') .should('be.visible')

View File

@ -460,7 +460,7 @@ describe('Glossary page should work properly', () => {
.and('be.visible') .and('be.visible')
.click(); .click();
cy.get('.ant-select-selection-item-remove').should('be.visible').click(); cy.get('[data-testid="remove-tags"]').should('be.visible').click();
interceptURL('PATCH', '/api/v1/glossaries/*', 'updateGlossary'); interceptURL('PATCH', '/api/v1/glossaries/*', 'updateGlossary');
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click(); cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
verifyResponseStatusCode('@updateGlossary', 200); verifyResponseStatusCode('@updateGlossary', 200);
@ -621,7 +621,7 @@ describe('Glossary page should work properly', () => {
visitEntityDetailsPage(entity.term, entity.serviceName, entity.entity); visitEntityDetailsPage(entity.term, entity.serviceName, entity.entity);
// Add tag to breadcrumb // Add tag to breadcrumb
cy.get('[data-testid="tag-container"] [data-testid="tags"]') cy.get('[data-testid="tag-container"] [data-testid="add-tag"]')
.eq(0) .eq(0)
.should('be.visible') .should('be.visible')
.click(); .click();
@ -660,8 +660,8 @@ describe('Glossary page should work properly', () => {
); );
// Add non mutually exclusive tags // Add non mutually exclusive tags
cy.get('[data-testid="tag-container"] [data-testid="tags"]') cy.get('[data-testid="entity-tags"] [data-testid="add-tag"]')
.eq(0) .scrollIntoView()
.should('be.visible') .should('be.visible')
.click(); .click();
@ -720,7 +720,10 @@ describe('Glossary page should work properly', () => {
).contains(term3); ).contains(term3);
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click(); cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
verifyResponseStatusCode('@countTag', 200); verifyResponseStatusCode('@countTag', 200);
cy.get(`[data-testid="tag-${glossary1}.${term3}"]`) cy.get(
`[data-row-key="comments"] [data-testid="tag-${glossary1}.${term3}"]`
)
.scrollIntoView()
.should('be.visible') .should('be.visible')
.contains(term3); .contains(term3);
@ -775,7 +778,7 @@ describe('Glossary page should work properly', () => {
.should('be.visible') .should('be.visible')
.click(); .click();
// Remove all added tags from breadcrumb // Remove all added tags from breadcrumb
cy.get('.ant-select-selection-item-remove') cy.get('[data-testid="remove-tags"]')
.should('be.visible') .should('be.visible')
.click({ multiple: true }); .click({ multiple: true });
@ -794,7 +797,9 @@ describe('Glossary page should work properly', () => {
.trigger('mouseover') .trigger('mouseover')
.click(); .click();
cy.get(`[title="${glossaryName}.${name}"] [data-testid="remove-tags"`) cy.get(
`[data-testid="selected-tag-${glossaryName}.${name}"] [data-testid="remove-tags"`
)
.should('be.visible') .should('be.visible')
.click(); .click();

View File

@ -206,7 +206,7 @@ describe('Tags page should work', () => {
verifyResponseStatusCode('@databaseSchemasPage', 200); verifyResponseStatusCode('@databaseSchemasPage', 200);
verifyResponseStatusCode('@permissions', 200); verifyResponseStatusCode('@permissions', 200);
cy.get('[data-testid="tags"] > [data-testid="add-tag"]') cy.get('[data-testid="tags"] [data-testid="add-tag"]')
.should('be.visible') .should('be.visible')
.click(); .click();
@ -228,18 +228,13 @@ describe('Tags page should work', () => {
cy.get('[data-testid="edit-button"]').should('exist').click(); cy.get('[data-testid="edit-button"]').should('exist').click();
// Remove all added tags // Remove all added tags
cy.get('.ant-select-selection-item-remove') cy.get('[data-testid="remove-tags"]').eq(0).should('be.visible').click();
.eq(0)
.should('be.visible')
.click();
interceptURL('PATCH', '/api/v1/databaseSchemas/*', 'removeTags'); interceptURL('PATCH', '/api/v1/databaseSchemas/*', 'removeTags');
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click(); cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
verifyResponseStatusCode('@removeTags', 200); verifyResponseStatusCode('@removeTags', 200);
cy.get('[data-testid="tags"] > [data-testid="add-tag"]').should( cy.get('[data-testid="tags"] [data-testid="add-tag"]').should('be.visible');
'be.visible'
);
}); });
it.skip('Add tag at DatabaseSchema level with task & suggestions', () => { it.skip('Add tag at DatabaseSchema level with task & suggestions', () => {
@ -317,18 +312,13 @@ describe('Tags page should work', () => {
cy.get('[data-testid="add-tag"]').should('exist').click(); cy.get('[data-testid="add-tag"]').should('exist').click();
// Remove all added tags // Remove all added tags
cy.get('.ant-select-selection-item-remove') cy.get('[data-testid="remove-tags"]').eq(0).should('be.visible').click();
.eq(0)
.should('be.visible')
.click();
interceptURL('PATCH', '/api/v1/databaseSchemas/*', 'removeTags'); interceptURL('PATCH', '/api/v1/databaseSchemas/*', 'removeTags');
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click(); cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
verifyResponseStatusCode('@removeTags', 200); verifyResponseStatusCode('@removeTags', 200);
cy.get('[data-testid="tags"] > [data-testid="add-tag"]').should( cy.get('[data-testid="tags"] [data-testid="add-tag"]').should('be.visible');
'be.visible'
);
}); });
it('Check Usage of tag and it should redirect to explore page with tags filter', () => { it('Check Usage of tag and it should redirect to explore page with tags filter', () => {

View File

@ -226,6 +226,7 @@ const MlModelFeaturesList: FC<MlModelFeaturesListProp> = ({
{`${t('label.tag-plural')}:`} {`${t('label.tag-plural')}:`}
</Typography.Text>{' '} </Typography.Text>{' '}
<div <div
className="w-min-20"
data-testid="feature-tags-wrapper" data-testid="feature-tags-wrapper"
onClick={() => handleTagContainerClick(feature)}> onClick={() => handleTagContainerClick(feature)}>
<TagsContainer <TagsContainer

View File

@ -0,0 +1,40 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import url('../../../styles/variables.less');
.tags-component-container {
.tag-container-style {
display: flex;
gap: 4px;
justify-content: center;
align-items: center;
font-size: 12px;
font-weight: 500;
cursor: pointer;
background-color: @white;
padding: 1px 8px;
margin: 1px 4px;
}
.label {
border: none;
background-color: transparent;
padding: 0px;
}
.outlined {
border: none;
background-color: transparent;
}
}

View File

@ -1,31 +0,0 @@
/*
* Copyright 2022 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const tagStyles = {
base: `relative tw-inline-flex text-xs font-medium
rounded-4 whitespace-nowrap h-6`,
contained: 'tw-bg-badge tw-mr-2 tw-my-0.5',
outlined: 'tw-bg-transparent tw-mr-2 tw-my-0.5',
label: 'tw-bg-transparent tw-border-none tw-text-grey-body',
border: 'tw-bg-white tw-border tw-items-center tw-mr-1 tw-mt-1',
text: {
base: 'tw-no-underline hover:tw-no-underline',
default: 'tw-px-2',
editable: 'tw-pl-2 tw-pr-1',
contained: 'tw-py-0.5 tw-px-2',
outlined: 'tw-py-0.5 tw-px-2',
border: 'tw-py-0.5 tw-px-2',
label: 'tw-px-1',
},
};

View File

@ -12,24 +12,21 @@
*/ */
import { CloseOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { Space, Tooltip } from 'antd'; import { Tag, Tooltip, Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants'; import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { ROUTES } from 'constants/constants'; import { ROUTES } from 'constants/constants';
import { TagSource } from 'generated/type/tagLabel'; import { TagSource } from 'generated/type/tagLabel';
import { isEmpty } from 'lodash';
import React, { FunctionComponent, useMemo } from 'react'; import React, { FunctionComponent, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { getTagDisplay } from 'utils/TagsUtils'; import { getTagDisplay, getTagTooltip } from 'utils/TagsUtils';
import { ReactComponent as IconPage } from '../../../assets/svg/ic-flat-doc.svg'; import { ReactComponent as IconPage } from '../../../assets/svg/ic-flat-doc.svg';
import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg'; import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg';
import { ReactComponent as IconTag } from '../../../assets/svg/tag-grey.svg'; import { ReactComponent as IconTag } from '../../../assets/svg/tag-grey.svg';
import { TAG_START_WITH } from 'constants/Tag.constants'; import { TAG_START_WITH } from 'constants/Tag.constants';
import { TagProps } from './tags.interface'; import { TagProps } from './tags.interface';
import { tagStyles } from './tags.styles'; import './Tags.less';
const Tags: FunctionComponent<TagProps> = ({ const Tags: FunctionComponent<TagProps> = ({
className, className,
@ -41,13 +38,7 @@ const Tags: FunctionComponent<TagProps> = ({
removeTag, removeTag,
isRemovable, isRemovable,
}: TagProps) => { }: TagProps) => {
const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
const baseStyle = tagStyles.base;
const layoutStyles = tagStyles[type];
const textBaseStyle = tagStyles.text.base;
const textLayoutStyles = tagStyles.text[type] || tagStyles.text.default;
const textEditStyles = editable ? tagStyles.text.editable : '';
const getTagString = (tag: string) => { const getTagString = (tag: string) => {
return tag.startsWith('#') ? tag.slice(1) : tag; return tag.startsWith('#') ? tag.slice(1) : tag;
@ -61,10 +52,18 @@ const Tags: FunctionComponent<TagProps> = ({
const startIcon = useMemo(() => { const startIcon = useMemo(() => {
switch (startWith) { switch (startWith) {
case TAG_START_WITH.PLUS: case TAG_START_WITH.PLUS:
return <PlusIcon height={16} name="plus" width={16} />; return (
<PlusIcon
className="flex-shrink"
height={16}
name="plus"
width={16}
/>
);
case TAG_START_WITH.SOURCE_ICON: case TAG_START_WITH.SOURCE_ICON:
return isGlossaryTag ? ( return isGlossaryTag ? (
<IconPage <IconPage
className="flex-shrink"
data-testid="glossary-icon" data-testid="glossary-icon"
height={12} height={12}
name="glossary-icon" name="glossary-icon"
@ -72,6 +71,7 @@ const Tags: FunctionComponent<TagProps> = ({
/> />
) : ( ) : (
<IconTag <IconTag
className="flex-shrink"
data-testid="tags-icon" data-testid="tags-icon"
height={12} height={12}
name="tag-icon" name="tag-icon"
@ -89,82 +89,55 @@ const Tags: FunctionComponent<TagProps> = ({
: tag.tagFQN; : tag.tagFQN;
return ( return (
<div <Tag
className={classNames(baseStyle, layoutStyles, className, 'tags-item')} className={classNames('tag-container-style', type, className)}
closable={editable && isRemovable}
closeIcon={<CloseOutlined className="tw-text-primary" />}
data-testid="tags" data-testid="tags"
icon={startIcon}
onClick={() => { onClick={() => {
if (tag.source && startWith !== TAG_START_WITH.PLUS) { if (tag.source && startWith !== TAG_START_WITH.PLUS) {
tag.source === TagSource.Glossary tag.source === TagSource.Glossary
? history.push(`${ROUTES.GLOSSARY}/${tag.tagFQN}`) ? history.push(`${ROUTES.GLOSSARY}/${tag.tagFQN}`)
: history.push(`${ROUTES.TAGS}/${tag.tagFQN.split('.')[0]}`); : history.push(`${ROUTES.TAGS}/${tag.tagFQN.split('.')[0]}`);
} }
}}
onClose={(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();
removeTag && removeTag(e, getTagString(tag.tagFQN));
}}> }}>
<Space <Typography.Paragraph
align="center" className="m-0"
className={classNames( data-testid={
textBaseStyle, startWith === TAG_START_WITH.PLUS ? 'add-tag' : `tag-${tag.tagFQN}`
textLayoutStyles, }
textEditStyles, style={{
'd-flex items-center cursor-pointer' display: 'inline-block',
)} whiteSpace: 'normal',
data-testid={editable ? `tag-${tag.tagFQN}` : 'add-tag'} wordBreak: 'break-all',
size={4}> }}>
{startIcon} {getTagDisplay(tagName)}
<span </Typography.Paragraph>
className={classNames( </Tag>
'text-xs font-medium',
startWith === '+' && 'text-primary'
)}>
{getTagDisplay(tagName)}
{editable && isRemovable && (
<span
className="tw-py-0.5 tw-px-2 tw-rounded tw-cursor-pointer"
data-testid={`remove-${tag}-tag`}
onClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();
removeTag && removeTag(e, getTagString(tag.tagFQN));
}}>
<CloseOutlined className="tw-text-primary" />
</span>
)}
</span>
</Space>
</div>
); );
}, [startIcon, tag, editable]); }, [startIcon, tag, editable]);
return ( return (
<> <div className="tags-component-container">
{startWith === TAG_START_WITH.PLUS ? ( {startWith === TAG_START_WITH.PLUS ? (
tagChip tagChip
) : ( ) : (
<Tooltip <Tooltip
className="cursor-pointer" className="cursor-pointer"
mouseEnterDelay={1.5}
placement="bottomLeft" placement="bottomLeft"
title={ title={getTagTooltip(tag.tagFQN, tag.description)}
<div className="text-left p-xss">
<div className="m-b-xs">
<RichTextEditorPreviewer
enableSeeMoreVariant={false}
markdown={
!isEmpty(tag.description)
? `**${tag.tagFQN}**\n${tag.description}`
: t('label.no-entity', {
entity: t('label.description'),
})
}
textVariant="white"
/>
</div>
</div>
}
trigger="hover"> trigger="hover">
{tagChip} {tagChip}
</Tooltip> </Tooltip>
)} )}
</> </div>
); );
}; };

View File

@ -12,13 +12,14 @@
*/ */
import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { Button, Select, Space, Tooltip, Typography } from 'antd'; import { Button, Select, Space, Tag, Tooltip, Typography } from 'antd';
import { ReactComponent as IconEdit } from 'assets/svg/edit-new.svg'; import { ReactComponent as IconEdit } from 'assets/svg/edit-new.svg';
import classNames from 'classnames'; import classNames from 'classnames';
import Tags from 'components/Tag/Tags/tags'; import Tags from 'components/Tag/Tags/tags';
import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants'; import { TAG_CONSTANT, TAG_START_WITH } from 'constants/Tag.constants';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { EntityTags, TagOption } from 'Models'; import { EntityTags, TagOption } from 'Models';
import type { CustomTagProps } from 'rc-select/lib/BaseSelect';
import React, { import React, {
FunctionComponent, FunctionComponent,
useCallback, useCallback,
@ -27,8 +28,8 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { getTagDisplay, getTagTooltip } from 'utils/TagsUtils';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { TagSource } from '../../../generated/type/tagLabel';
import { withLoader } from '../../../hoc/withLoader'; import { withLoader } from '../../../hoc/withLoader';
import Fqn from '../../../utils/Fqn'; import Fqn from '../../../utils/Fqn';
import { TagsContainerProps } from './tags-container.interface'; import { TagsContainerProps } from './tags-container.interface';
@ -117,7 +118,6 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
<Tags <Tags
editable editable
key={index} key={index}
showOnlyName={tag.source === TagSource.Glossary}
startWith={TAG_START_WITH.SOURCE_ICON} startWith={TAG_START_WITH.SOURCE_ICON}
tag={tag} tag={tag}
type="border" type="border"
@ -125,6 +125,45 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
); );
}; };
const tagRenderer = (customTagProps: CustomTagProps) => {
const { label, onClose } = customTagProps;
const tagLabel = getTagDisplay(label as string);
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
};
return (
<Tag
closable
className="text-sm flex-center m-r-xss p-r-xss m-y-2 border-light-gray"
closeIcon={
<CloseOutlined data-testid="remove-tags" height={8} width={8} />
}
data-testid={`selected-tag-${tagLabel}`}
onClose={onClose}
onMouseDown={onPreventMouseDown}>
<Tooltip
className="cursor-pointer"
mouseEnterDelay={1.5}
placement="topLeft"
title={getTagTooltip(label as string)}
trigger="hover">
<Typography.Paragraph
className="m-0"
style={{
display: 'inline-block',
whiteSpace: 'normal',
wordBreak: 'break-all',
}}>
{tagLabel}
</Typography.Paragraph>
</Tooltip>
</Tag>
);
};
useEffect(() => { useEffect(() => {
setTags(selectedTags); setTags(selectedTags);
}, [selectedTags]); }, [selectedTags]);
@ -174,7 +213,7 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
<> <>
<Select <Select
autoFocus autoFocus
className={classNames('flex-grow', className)} className={classNames('flex-grow w-max-95', className)}
data-testid="tag-selector" data-testid="tag-selector"
defaultValue={selectedTagsInternal} defaultValue={selectedTagsInternal}
mode="multiple" mode="multiple"
@ -185,12 +224,14 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
removeIcon={ removeIcon={
<CloseOutlined data-testid="remove-tags" height={8} width={8} /> <CloseOutlined data-testid="remove-tags" height={8} width={8} />
} }
tagRender={tagRenderer}
onChange={handleTagSelection}> onChange={handleTagSelection}>
{tagOptions.map(({ label, value, displayName }) => ( {tagOptions.map(({ label, value, displayName }) => (
<Select.Option key={label} value={value}> <Select.Option key={label} value={value}>
<Tooltip <Tooltip
destroyTooltipOnHide destroyTooltipOnHide
placement="topLeft" mouseEnterDelay={1.5}
placement="leftTop"
title={label} title={label}
trigger="hover"> trigger="hover">
{displayName} {displayName}

View File

@ -11,8 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import Tags from 'components/Tag/Tags/tags'; import { Typography } from 'antd';
import { TAG_CONSTANT } from 'constants/Tag.constants';
import React, { FunctionComponent, useEffect, useRef } from 'react'; import React, { FunctionComponent, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -69,14 +68,7 @@ const SearchOptions: FunctionComponent<SearchOptionsProp> = ({
to={getExplorePath({ search: searchText })} to={getExplorePath({ search: searchText })}
onClick={() => setIsOpen(false)}> onClick={() => setIsOpen(false)}>
{searchText} {searchText}
<Tags <Typography.Text>{t('label.in-open-metadata')}</Typography.Text>
className="tw-text-grey-body"
tag={{
...TAG_CONSTANT,
tagFQN: t('label.in-open-metadata'),
}}
type="outlined"
/>
</Link> </Link>
{options.map((option, index) => ( {options.map((option, index) => (
<span <span
@ -89,14 +81,7 @@ const SearchOptions: FunctionComponent<SearchOptionsProp> = ({
setIsOpen(false); setIsOpen(false);
}}> }}>
{searchText} {searchText}
<Tags <Typography.Text>{option}</Typography.Text>
className="tw-text-grey-body"
tag={{
...TAG_CONSTANT,
tagFQN: option,
}}
type="outlined"
/>
</span> </span>
))} ))}
</div> </div>

View File

@ -12,7 +12,7 @@
*/ */
import { StarOutlined } from '@ant-design/icons'; import { StarOutlined } from '@ant-design/icons';
import { Button, Dropdown, Popover, Space, Typography } from 'antd'; import { Button, Col, Dropdown, Popover, Row, Space, Typography } from 'antd';
import { ItemType } from 'antd/lib/menu/hooks/useItems'; import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import classNames from 'classnames'; import classNames from 'classnames';
@ -21,7 +21,7 @@ import VersionButton from 'components/VersionButton/VersionButton.component';
import { t } from 'i18next'; import { t } from 'i18next';
import { cloneDeep, isEmpty, isUndefined, toString } from 'lodash'; import { cloneDeep, isEmpty, isUndefined, toString } from 'lodash';
import { EntityTags, ExtraInfo, TagOption } from 'Models'; import { EntityTags, ExtraInfo, TagOption } from 'Models';
import React, { Fragment, useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { getActiveAnnouncement } from 'rest/feedsAPI'; import { getActiveAnnouncement } from 'rest/feedsAPI';
import { sortTagsCaseInsensitive } from 'utils/CommonUtils'; import { sortTagsCaseInsensitive } from 'utils/CommonUtils';
@ -233,30 +233,34 @@ const EntityPageInfo = ({
const getThreadElements = () => { const getThreadElements = () => {
if (!isUndefined(entityFieldThreads)) { if (!isUndefined(entityFieldThreads)) {
return !isUndefined(tagThread) ? ( return !isUndefined(tagThread) ? (
<Button <Col>
className="p-0 flex-center" <Button
data-testid="tag-thread" className="p-0 flex-center"
size="small" data-testid="tag-thread"
type="text" size="small"
onClick={() => onThreadLinkSelect?.(tagThread.entityLink)}> type="text"
<Space align="center" className="w-full h-full" size={2}> onClick={() => onThreadLinkSelect?.(tagThread.entityLink)}>
<IconComments height={16} name="comments" width={16} /> <Space align="center" className="w-full h-full" size={2}>
<span data-testid="tag-thread-count">{tagThread.count}</span> <IconComments height={16} name="comments" width={16} />
</Space> <span data-testid="tag-thread-count">{tagThread.count}</span>
</Button> </Space>
</Button>
</Col>
) : ( ) : (
<Button <Col>
className="p-0 flex-center" <Button
data-testid="start-tag-thread" className="p-0 flex-center"
icon={<IconCommentPlus height={16} name="comments" width={16} />} data-testid="start-tag-thread"
size="small" icon={<IconCommentPlus height={16} name="comments" width={16} />}
type="text" size="small"
onClick={() => type="text"
onThreadLinkSelect?.( onClick={() =>
getEntityFeedLink(entityType, entityFqn, 'tags') onThreadLinkSelect?.(
) getEntityFeedLink(entityType, entityFqn, 'tags')
} )
/> }
/>
</Col>
); );
} else { } else {
return null; return null;
@ -271,44 +275,48 @@ const EntityPageInfo = ({
return onThreadLinkSelect && return onThreadLinkSelect &&
TASK_ENTITIES.includes(entityType as EntityType) ? ( TASK_ENTITIES.includes(entityType as EntityType) ? (
<Button <Col>
className="p-0 flex-center" <Button
data-testid="request-entity-tags" className="p-0 flex-center"
size="small" data-testid="request-entity-tags"
type="text" size="small"
onClick={hasTags ? handleUpdateTags : handleRequestTags}> type="text"
<Popover onClick={hasTags ? handleUpdateTags : handleRequestTags}>
destroyTooltipOnHide <Popover
content={text} destroyTooltipOnHide
overlayClassName="ant-popover-request-description" content={text}
trigger="hover" overlayClassName="ant-popover-request-description"
zIndex={9999}> trigger="hover"
<IconRequest zIndex={9999}>
className="anticon" <IconRequest
height={16} className="anticon"
name="request-tags" height={16}
width={16} name="request-tags"
/> width={16}
</Popover> />
</Button> </Popover>
</Button>
</Col>
) : null; ) : null;
}, [tags]); }, [tags]);
const getTaskElement = useCallback(() => { const getTaskElement = useCallback(() => {
return !isUndefined(tagTask) ? ( return !isUndefined(tagTask) ? (
<Button <Col>
className="p-0 flex-center" <Button
data-testid="tag-task" className="p-0 flex-center"
size="small" data-testid="tag-task"
type="text" size="small"
onClick={() => type="text"
onThreadLinkSelect?.(tagTask.entityLink, ThreadType.Task) onClick={() =>
}> onThreadLinkSelect?.(tagTask.entityLink, ThreadType.Task)
<Space align="center" className="w-full h-full" size={2}> }>
<IconTaskColor height={16} name="comments" width={16} /> <Space align="center" className="w-full h-full" size={2}>
<span data-testid="tag-task-count">{tagTask.count}</span> <IconTaskColor height={16} name="comments" width={16} />
</Space> <span data-testid="tag-task-count">{tagTask.count}</span>
</Button> </Space>
</Button>
</Col>
) : null; ) : null;
}, [tagTask]); }, [tagTask]);
@ -441,47 +449,47 @@ const EntityPageInfo = ({
</span> </span>
))} ))}
</Space> </Space>
<Space wrap align="center" data-testid="entity-tags" size={6}> <Row align="middle" data-testid="entity-tags" gutter={8}>
{isTagEditable && !deleted && ( {isTagEditable && !deleted && (
<Fragment> <>
<Space <Col>
align="center" <Space
className="w-full h-full" align="center"
data-testid="tags-wrapper" className="w-full h-full"
size={8} data-testid="tags-wrapper"
onClick={() => { size={8}
// Fetch tags and terms only once onClick={() => {
if (tagList.length === 0) { // Fetch tags and terms only once
fetchTags(); if (tagList.length === 0) {
} fetchTags();
setIsEditable(true); }
}}> setIsEditable(true);
<TagsContainer }}>
showEditTagButton <TagsContainer
className="w-min-20" showEditTagButton
dropDownHorzPosRight={false} className="w-min-20"
editable={isEditable} dropDownHorzPosRight={false}
isLoading={isTagLoading} editable={isEditable}
selectedTags={getSelectedTags()} isLoading={isTagLoading}
showAddTagButton={getSelectedTags().length === 0} selectedTags={getSelectedTags()}
size="small" showAddTagButton={getSelectedTags().length === 0}
tagList={tagList} size="small"
onCancel={() => { tagList={tagList}
handleTagSelection(); onCancel={() => {
}} handleTagSelection();
onSelectionChange={(tags) => { }}
handleTagSelection(tags); onSelectionChange={(tags) => {
}} handleTagSelection(tags);
/> }}
</Space> />
<> </Space>
{getRequestTagsElements()} </Col>
{getTaskElement()} {getRequestTagsElements()}
{getThreadElements()} {getTaskElement()}
</> {getThreadElements()}
</Fragment> </>
)} )}
</Space> </Row>
</Space> </Space>
{activeAnnouncement && ( {activeAnnouncement && (
<AnnouncementCard <AnnouncementCard

View File

@ -15,7 +15,7 @@ import { Button, Divider, Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { AggregationEntry } from 'interface/search.interface'; import { AggregationEntry } from 'interface/search.interface';
import { isEmpty, isNil } from 'lodash'; import { isEmpty, isNil } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { getSortedTierBucketList } from 'utils/EntityUtils'; import { getSortedTierBucketList } from 'utils/EntityUtils';
@ -35,9 +35,6 @@ const FacetFilter: React.FC<FacetFilterProps> = ({
onClearFilter, onClearFilter,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [aggregationsPageSize, setAggregationsPageSize] = useState(
Object.fromEntries(Object.keys(aggregations).map((k) => [k, 5]))
);
/** /**
* Merging aggregations with filters. * Merging aggregations with filters.
* The aim is to ensure that if there a filter on aggregationKey `k` with value `v`, * The aim is to ensure that if there a filter on aggregationKey `k` with value `v`,
@ -100,18 +97,6 @@ const FacetFilter: React.FC<FacetFilterProps> = ({
.sort(([key1], [key2]) => compareAggregationKey(key1, key2)); .sort(([key1], [key2]) => compareAggregationKey(key1, key2));
}, [aggregations, filters]); }, [aggregations, filters]);
useEffect(() => {
if (!isEmpty(aggregations)) {
setAggregationsPageSize(
Object.fromEntries(
Object.keys(aggregations).map((k) =>
k in aggregationsPageSize ? [k, aggregationsPageSize[k]] : [k, 5]
)
)
);
}
}, [aggregations]);
return ( return (
<div data-testid="face-filter"> <div data-testid="face-filter">
<div className="sidebar-my-data-holder mt-2 mb-3 p-x-md"> <div className="sidebar-my-data-holder mt-2 mb-3 p-x-md">
@ -157,7 +142,9 @@ const FacetFilter: React.FC<FacetFilterProps> = ({
index, index,
{ length: aggregationsLength } { length: aggregationsLength }
) => ( ) => (
<div data-testid={`filter-heading-${aggregationKey}`} key={index}> <div
data-testid={`filter-heading-${aggregationKey}`}
key={aggregationKey}>
<div className="d-flex justify-between flex-col p-x-md"> <div className="d-flex justify-between flex-col p-x-md">
<Typography.Paragraph className="m-y-sm common-left-panel-card-heading"> <Typography.Paragraph className="m-y-sm common-left-panel-card-heading">
{translateAggregationKeyToTitle(aggregationKey)} {translateAggregationKeyToTitle(aggregationKey)}
@ -166,53 +153,20 @@ const FacetFilter: React.FC<FacetFilterProps> = ({
<div <div
className="sidebar-my-data-holder p-x-md" className="sidebar-my-data-holder p-x-md"
data-testid="filter-container"> data-testid="filter-container">
{aggregation.buckets {aggregation.buckets.slice(0, 10).map((bucket) => (
.slice(0, aggregationsPageSize[aggregationKey]) <FilterContainer
.map((bucket, index) => ( count={bucket.doc_count}
<FilterContainer isSelected={
count={bucket.doc_count} !isNil(filters) && aggregationKey in filters
isSelected={ ? filters[aggregationKey].includes(bucket.key)
!isNil(filters) && aggregationKey in filters : false
? filters[aggregationKey].includes(bucket.key) }
: false key={bucket.key}
} name={bucket.key}
key={index} type={aggregationKey}
name={bucket.key} onSelect={onSelectHandler}
type={aggregationKey} />
onSelect={onSelectHandler} ))}
/>
))}
<div className="m-y-sm">
{aggregationsPageSize[aggregationKey] <
aggregation.buckets.length && (
<p
className="link-text text-xs"
onClick={() =>
setAggregationsPageSize((prev) => ({
...prev,
[aggregationKey]: prev[aggregationKey] + 5,
}))
}>
{t('label.view-entity', {
entity: t('label.more-lowercase'),
})}
</p>
)}
{aggregationsPageSize[aggregationKey] > 5 && (
<p
className="link-text text-xs text-left"
onClick={() =>
setAggregationsPageSize((prev) => ({
...prev,
[aggregationKey]: Math.max(5, prev[aggregationKey] - 5),
}))
}>
{t('label.view-entity', {
entity: t('label.less-lowercase'),
})}
</p>
)}
</div>
</div> </div>
{index !== aggregationsLength - 1 && <Divider className="m-0" />} {index !== aggregationsLength - 1 && <Divider className="m-0" />}
</div> </div>

View File

@ -34,7 +34,7 @@ const FilterContainer: FunctionComponent<FilterContainerProp> = ({
: name; : name;
return ( return (
<Tooltip placement="top" title={formattedName} trigger="hover"> <Tooltip placement="topLeft" title={formattedName} trigger="hover">
{label || formattedName} {label || formattedName}
</Tooltip> </Tooltip>
); );

View File

@ -540,7 +540,7 @@ const AddAlertPage = () => {
style={{ margin: 0, marginBottom: '16px' }} style={{ margin: 0, marginBottom: '16px' }}
/> />
)} )}
<div className="d-flex gap-1"> <div className="d-flex gap-4">
<div className="flex-1"> <div className="flex-1">
<Form.Item key={key} name={[name, 'name']}> <Form.Item key={key} name={[name, 'name']}>
<Select <Select

View File

@ -127,6 +127,9 @@
.border-gray { .border-gray {
border-color: @gray; border-color: @gray;
} }
.border-light-gray {
border-color: @light-border-color;
}
.line-height-16 { .line-height-16 {
line-height: 16px; line-height: 16px;

View File

@ -86,8 +86,11 @@
.w-max-1080 { .w-max-1080 {
max-width: 1080px; max-width: 1080px;
} }
.w-500 { .w-max-fit-content {
width: 500px; max-width: fit-content;
}
.w-max-95 {
max-width: 95%;
} }
.w-300 { .w-300 {
width: 300px; width: 300px;

View File

@ -95,6 +95,9 @@
} }
.gap-1 { .gap-1 {
gap: 4px;
}
.gap-4 {
gap: 16px; gap: 16px;
} }

View File

@ -58,6 +58,10 @@
margin-top: 0 !important; margin-top: 0 !important;
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.m-y-2 {
margin-top: 2px;
margin-bottom: 2px;
}
.m-y-xs { .m-y-xs {
margin-top: @margin-xs; margin-top: @margin-xs;
margin-bottom: @margin-xs; margin-bottom: @margin-xs;

View File

@ -50,3 +50,4 @@
@announcement-border: #ffc143; @announcement-border: #ffc143;
@test-parameter-bg-color: #e7ebf0; @test-parameter-bg-color: #e7ebf0;
@group-title-color: #76746f; @group-title-color: #76746f;
@light-border-color: #f0f0f0;

View File

@ -11,8 +11,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { OPEN_METADATA } from 'constants/service-guide.constant';
import { isUndefined, startCase } from 'lodash'; import { isUndefined, startCase } from 'lodash';
import { IngestionPipeline } from '../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { IngestionPipeline } from '../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { getSettingsPathFromPipelineType } from './IngestionUtils';
import { getLogEntityPath } from './RouterUtils'; import { getLogEntityPath } from './RouterUtils';
/** /**
@ -30,6 +32,20 @@ export const getLogBreadCrumbs = (
ingestionName: string, ingestionName: string,
ingestionDetails: IngestionPipeline | undefined ingestionDetails: IngestionPipeline | undefined
) => { ) => {
if (ingestionName.split('.')[0] === OPEN_METADATA && ingestionDetails) {
return [
{
name: startCase(ingestionDetails.pipelineType),
url: getSettingsPathFromPipelineType(ingestionDetails.pipelineType),
activeTitle: true,
},
{
name: startCase(ingestionName.split('.')[1]),
url: '',
activeTitle: true,
},
];
}
if (isUndefined(ingestionDetails)) { if (isUndefined(ingestionDetails)) {
return []; return [];
} }

View File

@ -13,16 +13,18 @@
import { RuleObject } from 'antd/lib/form'; import { RuleObject } from 'antd/lib/form';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { delimiterRegex } from 'constants/regex.constants'; import { delimiterRegex } from 'constants/regex.constants';
import i18next from 'i18next'; import i18next from 'i18next';
import { isEmpty, isUndefined, toLower } from 'lodash'; import { isEmpty, isUndefined, toLower } from 'lodash';
import { Bucket, EntityTags, TagOption } from 'Models'; import { Bucket, EntityTags, TagOption } from 'Models';
import React from 'react';
import { import {
getAllClassifications, getAllClassifications,
getClassificationByName, getClassificationByName,
getTags, getTags,
} from 'rest/tagAPI'; } from 'rest/tagAPI';
import { TAG_VIEW_CAP } from '../constants/constants';
import { SettledStatus } from '../enums/axios.enum'; import { SettledStatus } from '../enums/axios.enum';
import { Classification } from '../generated/entity/classification/classification'; import { Classification } from '../generated/entity/classification/classification';
import { Tag } from '../generated/entity/classification/tag'; import { Tag } from '../generated/entity/classification/tag';
@ -179,7 +181,15 @@ export const getTagsWithLabel = (tags: Array<Bucket>) => {
// Will return tag with ellipses if it exceeds the limit // Will return tag with ellipses if it exceeds the limit
export const getTagDisplay = (tag: string) => { export const getTagDisplay = (tag: string) => {
return tag.length > TAG_VIEW_CAP ? `${tag.slice(0, TAG_VIEW_CAP)}...` : tag; const tagLevelsArray = tag.split(FQN_SEPARATOR_CHAR);
if (tagLevelsArray.length > 3) {
return `${tagLevelsArray[0]}...${tagLevelsArray
.slice(-2)
.join(FQN_SEPARATOR_CHAR)}`;
}
return tag;
}; };
export const fetchTagsAndGlossaryTerms = async () => { export const fetchTagsAndGlossaryTerms = async () => {
@ -250,3 +260,15 @@ export const tagsNameValidator =
return Promise.resolve(); return Promise.resolve();
}; };
export const getTagTooltip = (fqn: string, description?: string) => (
<div className="text-left p-xss">
<div className="m-b-xs">
<RichTextEditorPreviewer
enableSeeMoreVariant={false}
markdown={`**${fqn}**\n${description ?? ''}`}
textVariant="white"
/>
</div>
</div>
);