Minor search issues (#14861)

* fix: minor search issues

* show dbt icon in lineage
This commit is contained in:
Karan Hotchandani 2024-01-25 19:08:02 +05:30 committed by GitHub
parent 6a1c13687e
commit 00838d6c73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 140 additions and 56 deletions

View File

@ -237,6 +237,7 @@ const CustomControls: FC<ControlProps> = ({
</Button> </Button>
</Dropdown> </Dropdown>
<ExploreQuickFilters <ExploreQuickFilters
independent
aggregations={{}} aggregations={{}}
fields={selectedQuickFilters} fields={selectedQuickFilters}
index={SearchIndex.ALL} index={SearchIndex.ALL}

View File

@ -13,6 +13,7 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { ReactFlowProvider } from 'reactflow'; import { ReactFlowProvider } from 'reactflow';
import { ModelType } from '../../../generated/entity/data/table';
import CustomNodeV1Component from './CustomNodeV1.component'; import CustomNodeV1Component from './CustomNodeV1.component';
const mockNodeDataProps = { const mockNodeDataProps = {
@ -39,6 +40,33 @@ const mockNodeDataProps = {
zIndex: 0, zIndex: 0,
}; };
const mockNodeDataProps2 = {
id: 'node1',
type: 'table',
data: {
node: {
fullyQualifiedName: 'dim_customer',
type: 'table',
entityType: 'table',
id: 'khjahjfja',
columns: [
{ fullyQualifiedName: 'col1', name: 'col1' },
{ fullyQualifiedName: 'col2', name: 'col2' },
{ fullyQualifiedName: 'col3', name: 'col3' },
],
dataModel: {
modelType: ModelType.Dbt,
},
},
},
selected: false,
isConnectable: false,
xPos: 0,
yPos: 0,
dragging: true,
zIndex: 0,
};
const onMockColumnClick = jest.fn(); const onMockColumnClick = jest.fn();
jest.mock('../../LineageProvider/LineageProvider', () => ({ jest.mock('../../LineageProvider/LineageProvider', () => ({
@ -74,4 +102,16 @@ describe('CustomNodeV1', () => {
expect(screen.getByTestId('lineage-node-dim_customer')).toBeInTheDocument(); expect(screen.getByTestId('lineage-node-dim_customer')).toBeInTheDocument();
expect(screen.getByTestId('expand-cols-btn')).toBeInTheDocument(); expect(screen.getByTestId('expand-cols-btn')).toBeInTheDocument();
}); });
it('renders node with dbt icon correctly', () => {
render(
<ReactFlowProvider>
<CustomNodeV1Component {...mockNodeDataProps2} />
</ReactFlowProvider>
);
expect(screen.getByTestId('lineage-node-dim_customer')).toBeInTheDocument();
expect(screen.getByTestId('expand-cols-btn')).toBeInTheDocument();
expect(screen.getByTestId('dbt-icon')).toBeInTheDocument();
});
}); });

View File

@ -12,33 +12,27 @@
*/ */
import { Col, Row, Space, Typography } from 'antd'; import { Col, Row, Space, Typography } from 'antd';
import { get } from 'lodash'; import React, { useMemo } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { EntityLineageNodeType } from '../../../enums/entity.enum'; import { ReactComponent as IconDBTModel } from '../../../assets/svg/dbt-model.svg';
import { EntityReference } from '../../../generated/entity/type'; import { EntityType } from '../../../enums/entity.enum';
import { ModelType, Table } from '../../../generated/entity/data/table';
import { getBreadcrumbsFromFqn } from '../../../utils/EntityUtils'; import { getBreadcrumbsFromFqn } from '../../../utils/EntityUtils';
import { getServiceIcon } from '../../../utils/TableUtils'; import { getServiceIcon } from '../../../utils/TableUtils';
import { SourceType } from '../../SearchedData/SearchedData.interface'; import { SourceType } from '../../SearchedData/SearchedData.interface';
import './lineage-node-label.less'; import './lineage-node-label.less';
interface LineageNodeLabelProps { interface LineageNodeLabelProps {
node: EntityReference; node: SourceType;
} }
const EntityLabel = ({ node }: Pick<LineageNodeLabelProps, 'node'>) => { const EntityLabel = ({ node }: Pick<LineageNodeLabelProps, 'node'>) => {
const { t } = useTranslation(); const showDbtIcon = useMemo(() => {
if (node.type === EntityLineageNodeType.LOAD_MORE) {
return ( return (
<div> (node as SourceType).entityType === EntityType.TABLE &&
<span>{t('label.load-more')}</span> (node as Table)?.dataModel?.modelType === ModelType.Dbt
<span className="load-more-node-sizes p-x-xs">{`(${get(
node,
'pagination_data.childrenLength'
)})`}</span>
</div>
); );
} }, [node]);
return ( return (
<Row className="items-center" wrap={false}> <Row className="items-center" wrap={false}>
@ -48,29 +42,34 @@ const EntityLabel = ({ node }: Pick<LineageNodeLabelProps, 'node'>) => {
</div> </div>
<Space align="start" direction="vertical" size={0}> <Space align="start" direction="vertical" size={0}>
<Typography.Text <Typography.Text
className="m-b-0 d-block text-left text-grey-muted w-56" className="m-b-0 d-block text-left text-grey-muted w-54"
data-testid="entity-header-name" data-testid="entity-header-name"
ellipsis={{ tooltip: true }}> ellipsis={{ tooltip: true }}>
{node.name} {node.name}
</Typography.Text> </Typography.Text>
<Typography.Text <Typography.Text
className="m-b-0 d-block text-left entity-header-display-name text-md font-medium w-56" className="m-b-0 d-block text-left entity-header-display-name text-md font-medium w-54"
data-testid="entity-header-display-name" data-testid="entity-header-display-name"
ellipsis={{ tooltip: true }}> ellipsis={{ tooltip: true }}>
{node.displayName || node.name} {node.displayName || node.name}
</Typography.Text> </Typography.Text>
</Space> </Space>
{showDbtIcon && (
<div className="m-r-xs" data-testid="dbt-icon">
<IconDBTModel />
</div>
)}
</Col> </Col>
</Row> </Row>
); );
}; };
const LineageNodeLabelV1 = ({ node }: { node: EntityReference }) => { const LineageNodeLabelV1 = ({ node }: Pick<LineageNodeLabelProps, 'node'>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const breadcrumbs = getBreadcrumbsFromFqn(node.fullyQualifiedName ?? ''); const breadcrumbs = getBreadcrumbsFromFqn(node.fullyQualifiedName ?? '');
return ( return (
<div className="w-72"> <div className="w-76">
<div className="m-0 p-x-md p-y-xs"> <div className="m-0 p-x-md p-y-xs">
<div className="d-flex gap-2 items-center m-b-xs"> <div className="d-flex gap-2 items-center m-b-xs">
<Space <Space

View File

@ -115,7 +115,7 @@ const NodeSuggestions: FC<EntitySuggestionProps> = ({
autoFocus autoFocus
open open
showSearch showSearch
className="w-72 lineage-node-searchbox" className="w-76 lineage-node-searchbox"
data-testid="node-search-box" data-testid="node-search-box"
options={(data || []).map((entity) => ({ options={(data || []).map((entity) => ({
value: entity.fullyQualifiedName, value: entity.fullyQualifiedName,

View File

@ -23,6 +23,7 @@ export interface ExploreQuickFiltersProps {
onAdvanceSearch?: () => void; onAdvanceSearch?: () => void;
showDeleted?: boolean; showDeleted?: boolean;
onChangeShowDeleted?: (showDeleted: boolean) => void; onChangeShowDeleted?: (showDeleted: boolean) => void;
independent?: boolean; // flag to indicate if the filters are independent of aggregations
} }
export interface FilterFieldsMenuItem { export interface FilterFieldsMenuItem {

View File

@ -41,6 +41,7 @@ const ExploreQuickFilters: FC<ExploreQuickFiltersProps> = ({
fields, fields,
index, index,
aggregations, aggregations,
independent = false,
onFieldValueSelect, onFieldValueSelect,
}) => { }) => {
const location = useLocation(); const location = useLocation();
@ -151,7 +152,7 @@ const ExploreQuickFilters: FC<ExploreQuickFiltersProps> = ({
return; return;
} }
if (aggregations?.[key] && key !== TIER_FQN_KEY) { if (key !== TIER_FQN_KEY) {
const res = await getAggregateFieldOptions( const res = await getAggregateFieldOptions(
index, index,
key, key,
@ -197,6 +198,7 @@ const ExploreQuickFilters: FC<ExploreQuickFiltersProps> = ({
<SearchDropdown <SearchDropdown
highlight highlight
fixedOrderOptions={field.key === TIER_FQN_KEY} fixedOrderOptions={field.key === TIER_FQN_KEY}
independent={independent}
index={index as ExploreSearchIndex} index={index as ExploreSearchIndex}
isSuggestionsLoading={isOptionsLoading} isSuggestionsLoading={isOptionsLoading}
key={field.key} key={field.key}

View File

@ -26,6 +26,7 @@ export interface SearchDropdownProps {
onChange: (values: SearchDropdownOption[], searchKey: string) => void; onChange: (values: SearchDropdownOption[], searchKey: string) => void;
onGetInitialOptions?: (searchKey: string) => void; onGetInitialOptions?: (searchKey: string) => void;
onSearch: (searchText: string, searchKey: string) => void; onSearch: (searchText: string, searchKey: string) => void;
independent?: boolean; // flag to indicate if the filters are independent of aggregations
} }
export interface SearchDropdownOption { export interface SearchDropdownOption {

View File

@ -63,6 +63,7 @@ const SearchDropdown: FC<SearchDropdownProps> = ({
onGetInitialOptions, onGetInitialOptions,
onSearch, onSearch,
index, index,
independent = false,
}) => { }) => {
const tabsInfo = searchClassBase.getTabsInfo(); const tabsInfo = searchClassBase.getTabsInfo();
const { t } = useTranslation(); const { t } = useTranslation();
@ -75,7 +76,9 @@ const SearchDropdown: FC<SearchDropdownProps> = ({
// derive menu props from options and selected keys // derive menu props from options and selected keys
const menuOptions: MenuProps['items'] = useMemo(() => { const menuOptions: MenuProps['items'] = useMemo(() => {
// Filtering out selected options // Filtering out selected options
const selectedOptionsObj = options.filter((option) => const selectedOptionsObj = independent
? selectedOptions
: options.filter((option) =>
selectedOptions.find((selectedOpt) => option.key === selectedOpt.key) selectedOptions.find((selectedOpt) => option.key === selectedOpt.key)
); );
@ -116,7 +119,7 @@ const SearchDropdown: FC<SearchDropdownProps> = ({
return [...selectedOptionKeys, ...otherOptions]; return [...selectedOptionKeys, ...otherOptions];
} }
}, [options, selectedOptions, fixedOrderOptions]); }, [options, selectedOptions, fixedOrderOptions, independent]);
// handle menu item click // handle menu item click
const handleMenuItemClick: MenuItemProps['onClick'] = (info) => { const handleMenuItemClick: MenuItemProps['onClick'] = (info) => {

View File

@ -53,7 +53,6 @@ import { findActiveSearchIndex } from '../../utils/Explore.utils';
import { getCombinedQueryFilterObject } from '../../utils/ExplorePage/ExplorePageUtils'; import { getCombinedQueryFilterObject } from '../../utils/ExplorePage/ExplorePageUtils';
import searchClassBase from '../../utils/SearchClassBase'; import searchClassBase from '../../utils/SearchClassBase';
import { escapeESReservedCharacters } from '../../utils/StringsUtils'; import { escapeESReservedCharacters } from '../../utils/StringsUtils';
import { getEntityIcon } from '../../utils/TableUtils';
import { showErrorToast } from '../../utils/ToastUtils'; import { showErrorToast } from '../../utils/ToastUtils';
import { import {
QueryFieldInterface, QueryFieldInterface,
@ -229,7 +228,10 @@ const ExplorePageV1: FunctionComponent = () => {
const tabItems = useMemo(() => { const tabItems = useMemo(() => {
const items = Object.entries(tabsInfo).map( const items = Object.entries(tabsInfo).map(
([tabSearchIndex, tabDetail]) => ({ ([tabSearchIndex, tabDetail]) => {
const Icon = tabDetail.icon;
return {
key: tabSearchIndex, key: tabSearchIndex,
label: ( label: (
<div <div
@ -237,7 +239,7 @@ const ExplorePageV1: FunctionComponent = () => {
data-testid={`${lowerCase(tabDetail.label)}-tab`}> data-testid={`${lowerCase(tabDetail.label)}-tab`}>
<div className="d-flex items-center"> <div className="d-flex items-center">
<span className="explore-icon d-flex m-r-xs"> <span className="explore-icon d-flex m-r-xs">
{getEntityIcon(tabSearchIndex)} <Icon />
</span> </span>
<Typography.Text <Typography.Text
className={ className={
@ -260,7 +262,8 @@ const ExplorePageV1: FunctionComponent = () => {
count: searchHitCounts count: searchHitCounts
? searchHitCounts[tabSearchIndex as ExploreSearchIndex] ? searchHitCounts[tabSearchIndex as ExploreSearchIndex]
: 0, : 0,
}) };
}
); );
return searchQueryParam return searchQueryParam

View File

@ -59,6 +59,9 @@
.w-52 { .w-52 {
width: 13rem; /* 208px */ width: 13rem; /* 208px */
} }
.w-54 {
width: 13.5rem;
}
.w-56 { .w-56 {
width: 14rem; width: 14rem;
} }
@ -71,6 +74,9 @@
.w-72 { .w-72 {
width: 288px; width: 288px;
} }
.w-76 {
width: 19rem;
}
.w-400 { .w-400 {
width: 400px; width: 400px;
} }

View File

@ -10,7 +10,21 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { SearchOutlined } from '@ant-design/icons';
import i18next from 'i18next'; import i18next from 'i18next';
import { ReactComponent as ClassificationIcon } from '../assets/svg/classification.svg';
import { ReactComponent as IconDataModel } from '../assets/svg/data-model.svg';
import { ReactComponent as GlossaryIcon } from '../assets/svg/glossary.svg';
import { ReactComponent as DashboardIcon } from '../assets/svg/ic-dashboard.svg';
import { ReactComponent as DataProductIcon } from '../assets/svg/ic-data-product.svg';
import { ReactComponent as DatabaseIcon } from '../assets/svg/ic-database.svg';
import { ReactComponent as MlModelIcon } from '../assets/svg/ic-ml-model.svg';
import { ReactComponent as PipelineIcon } from '../assets/svg/ic-pipeline.svg';
import { ReactComponent as SchemaIcon } from '../assets/svg/ic-schema.svg';
import { ReactComponent as ContainerIcon } from '../assets/svg/ic-storage.svg';
import { ReactComponent as IconStoredProcedure } from '../assets/svg/ic-stored-procedure.svg';
import { ReactComponent as TableIcon } from '../assets/svg/ic-table.svg';
import { ReactComponent as TopicIcon } from '../assets/svg/ic-topic.svg';
import { ReactComponent as IconTable } from '../assets/svg/table-grey.svg'; import { ReactComponent as IconTable } from '../assets/svg/table-grey.svg';
import { import {
Option, Option,
@ -82,84 +96,98 @@ class SearchClassBase {
sortingFields: tableSortingFields, sortingFields: tableSortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'tables', path: 'tables',
icon: TableIcon,
}, },
[SearchIndex.STORED_PROCEDURE]: { [SearchIndex.STORED_PROCEDURE]: {
label: i18n.t('label.stored-procedure-plural'), label: i18n.t('label.stored-procedure-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'storedProcedure', path: 'storedProcedure',
icon: IconStoredProcedure,
}, },
[SearchIndex.DATABASE]: { [SearchIndex.DATABASE]: {
label: i18n.t('label.database-plural'), label: i18n.t('label.database-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'databases', path: 'databases',
icon: DatabaseIcon,
}, },
[SearchIndex.DATABASE_SCHEMA]: { [SearchIndex.DATABASE_SCHEMA]: {
label: i18n.t('label.database-schema-plural'), label: i18n.t('label.database-schema-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'databaseSchemas', path: 'databaseSchemas',
icon: SchemaIcon,
}, },
[SearchIndex.DASHBOARD]: { [SearchIndex.DASHBOARD]: {
label: i18n.t('label.dashboard-plural'), label: i18n.t('label.dashboard-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'dashboards', path: 'dashboards',
icon: DashboardIcon,
}, },
[SearchIndex.DASHBOARD_DATA_MODEL]: { [SearchIndex.DASHBOARD_DATA_MODEL]: {
label: i18n.t('label.dashboard-data-model-plural'), label: i18n.t('label.dashboard-data-model-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'dashboardDataModel', path: 'dashboardDataModel',
icon: IconDataModel,
}, },
[SearchIndex.PIPELINE]: { [SearchIndex.PIPELINE]: {
label: i18n.t('label.pipeline-plural'), label: i18n.t('label.pipeline-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'pipelines', path: 'pipelines',
icon: PipelineIcon,
}, },
[SearchIndex.TOPIC]: { [SearchIndex.TOPIC]: {
label: i18n.t('label.topic-plural'), label: i18n.t('label.topic-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'topics', path: 'topics',
icon: TopicIcon,
}, },
[SearchIndex.MLMODEL]: { [SearchIndex.MLMODEL]: {
label: i18n.t('label.ml-model-plural'), label: i18n.t('label.ml-model-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'mlmodels', path: 'mlmodels',
icon: MlModelIcon,
}, },
[SearchIndex.CONTAINER]: { [SearchIndex.CONTAINER]: {
label: i18n.t('label.container-plural'), label: i18n.t('label.container-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'containers', path: 'containers',
icon: ContainerIcon,
}, },
[SearchIndex.SEARCH_INDEX]: { [SearchIndex.SEARCH_INDEX]: {
label: i18n.t('label.search-index-plural'), label: i18n.t('label.search-index-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'searchIndexes', path: 'searchIndexes',
icon: SearchOutlined,
}, },
[SearchIndex.GLOSSARY]: { [SearchIndex.GLOSSARY]: {
label: i18n.t('label.glossary-plural'), label: i18n.t('label.glossary-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'glossaries', path: 'glossaries',
icon: GlossaryIcon,
}, },
[SearchIndex.TAG]: { [SearchIndex.TAG]: {
label: i18n.t('label.tag-plural'), label: i18n.t('label.tag-plural'),
sortingFields: entitySortingFields, sortingFields: entitySortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'tags', path: 'tags',
icon: ClassificationIcon,
}, },
[SearchIndex.DATA_PRODUCT]: { [SearchIndex.DATA_PRODUCT]: {
label: i18n.t('label.data-product-plural'), label: i18n.t('label.data-product-plural'),
sortingFields: tableSortingFields, sortingFields: tableSortingFields,
sortField: INITIAL_SORT_FIELD, sortField: INITIAL_SORT_FIELD,
path: 'dataProducts', path: 'dataProducts',
icon: DataProductIcon,
}, },
}; };
} }