feat: highlight search text functionality (#19405)

* feat: highlight search text functionality

* test: add unit tests

* fix: mock implementation of functions

* fix: quality issues

* test: add test cases for IngestionListTableUtils and minor refactor

* refactor: name and displayName values
This commit is contained in:
Pranita Fulsundar 2025-01-30 12:03:22 +05:30 committed by GitHub
parent 4a6c613c56
commit 7afecdd59c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 278 additions and 53 deletions

View File

@ -41,7 +41,11 @@ import {
patchDatabaseSchemaDetails,
} from '../../../../rest/databaseAPI';
import { searchQuery } from '../../../../rest/searchAPI';
import { getEntityName } from '../../../../utils/EntityUtils';
import {
getEntityName,
highlightSearchText,
} from '../../../../utils/EntityUtils';
import { stringToHTML } from '../../../../utils/StringsUtils';
import { getUsagePercentile } from '../../../../utils/TableUtils';
import { showErrorToast } from '../../../../utils/ToastUtils';
import DisplayName from '../../../common/DisplayName/DisplayName';
@ -221,7 +225,9 @@ export const DatabaseSchemaTable = ({
render: (_, record: DatabaseSchema) => (
<DisplayName
allowRename={allowEditDisplayNamePermission}
displayName={record.displayName}
displayName={stringToHTML(
highlightSearchText(record.displayName, searchValue)
)}
id={record.id ?? ''}
key={record.id}
link={
@ -232,7 +238,7 @@ export const DatabaseSchemaTable = ({
)
: ''
}
name={record.name}
name={stringToHTML(highlightSearchText(record.name, searchValue))}
onEditDisplayName={handleDisplayNameUpdate}
/>
),

View File

@ -33,4 +33,5 @@ export interface ExploreSearchCardProps {
hideBreadcrumbs?: boolean;
actionPopoverContent?: React.ReactNode;
onCheckboxChange?: (checked: boolean) => void;
searchValue?: string;
}

View File

@ -28,7 +28,7 @@ import { Table } from '../../../generated/entity/data/table';
import { EntityReference } from '../../../generated/entity/type';
import { TagLabel } from '../../../generated/tests/testCase';
import { AssetCertification } from '../../../generated/type/assetCertification';
import { getEntityName } from '../../../utils/EntityUtils';
import { getEntityName, highlightSearchText } from '../../../utils/EntityUtils';
import { getDomainPath } from '../../../utils/RouterUtils';
import searchClassBase from '../../../utils/SearchClassBase';
import { stringToHTML } from '../../../utils/StringsUtils';
@ -61,6 +61,7 @@ const ExploreSearchCard: React.FC<ExploreSearchCardProps> = forwardRef<
showCheckboxes = false,
checked = false,
onCheckboxChange,
searchValue,
},
ref
) => {
@ -222,7 +223,12 @@ const ExploreSearchCard: React.FC<ExploreSearchCardProps> = forwardRef<
<Typography.Text
className="text-lg font-medium text-link-color"
data-testid="entity-header-display-name">
{stringToHTML(searchClassBase.getEntityName(source))}
{stringToHTML(
highlightSearchText(
searchClassBase.getEntityName(source),
searchValue
)
)}
</Typography.Text>
</Button>
) : (
@ -250,7 +256,12 @@ const ExploreSearchCard: React.FC<ExploreSearchCardProps> = forwardRef<
<Typography.Text
className="text-lg font-medium text-link-color break-word whitespace-normal"
data-testid="entity-header-display-name">
{stringToHTML(searchClassBase.getEntityName(source))}
{stringToHTML(
highlightSearchText(
searchClassBase.getEntityName(source),
searchValue
)
)}
</Typography.Text>
</Link>
@ -296,7 +307,10 @@ const ExploreSearchCard: React.FC<ExploreSearchCardProps> = forwardRef<
<div className="p-t-sm">
<TableDataCardBody
description={source.description ?? ''}
description={highlightSearchText(
source.description ?? '',
searchValue
)}
extraInfo={otherDetails}
tags={showTags ? source.tags : []}
/>

View File

@ -630,6 +630,7 @@ const AssetsTabs = forwardRef(
handleSummaryPanelDisplay={setSelectedCard}
id={_id}
key={'assets_' + _id}
searchValue={searchValue}
showCheckboxes={Boolean(activeEntity) && permissions.Create}
showTags={false}
source={_source}

View File

@ -29,6 +29,17 @@ jest.mock('../../../../hoc/LimitWrapper', () => {
return jest.fn().mockImplementation(() => <>LimitWrapper</>);
});
jest.mock('../../../../utils/StringsUtils', () => ({
...jest.requireActual('../../../../utils/StringsUtils'),
stringToHTML: jest.fn((text) => text),
}));
jest.mock('../../../../utils/EntityUtils', () => ({
...jest.requireActual('../../../../utils/EntityUtils'),
highlightSearchText: jest.fn((text) => text),
getTitleCase: jest.fn((text) => text.charAt(0).toUpperCase() + text.slice(1)),
}));
describe('BotListV1', () => {
it('renders the component', () => {
render(<BotListV1 {...mockProps} />, { wrapper: MemoryRouter });

View File

@ -34,8 +34,12 @@ import LimitWrapper from '../../../../hoc/LimitWrapper';
import { useAuth } from '../../../../hooks/authHooks';
import { usePaging } from '../../../../hooks/paging/usePaging';
import { getBots } from '../../../../rest/botsAPI';
import { getEntityName } from '../../../../utils/EntityUtils';
import {
getEntityName,
highlightSearchText,
} from '../../../../utils/EntityUtils';
import { getSettingPageEntityBreadCrumb } from '../../../../utils/GlobalSettingsUtils';
import { stringToHTML } from '../../../../utils/StringsUtils';
import { showErrorToast } from '../../../../utils/ToastUtils';
import DeleteWidgetModal from '../../../common/DeleteWidget/DeleteWidgetModal';
import ErrorPlaceHolder from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
@ -73,6 +77,7 @@ const BotListV1 = ({
const [handleErrorPlaceholder, setHandleErrorPlaceholder] = useState(false);
const [searchedData, setSearchedData] = useState<Bot[]>([]);
const [searchTerm, setSearchTerm] = useState<string>('');
const breadcrumbs: TitleBreadcrumbProps['titleLinks'] = useMemo(
() => getSettingPageEntityBreadCrumb(GlobalSettingsMenuCategory.BOTS),
@ -123,7 +128,7 @@ const BotListV1 = ({
return (
<Link data-testid={`bot-link-${name}`} to={getBotsPath(fqn)}>
{name}
{stringToHTML(highlightSearchText(name, searchTerm))}
</Link>
);
},
@ -134,7 +139,12 @@ const BotListV1 = ({
key: 'description',
render: (_, record) =>
record?.description ? (
<RichTextEditorPreviewerV1 markdown={record?.description || ''} />
<RichTextEditorPreviewerV1
markdown={highlightSearchText(
record?.description || '',
searchTerm
)}
/>
) : (
<span data-testid="no-description">
{t('label.no-entity', {
@ -205,6 +215,7 @@ const BotListV1 = ({
}, [selectedUser]);
const handleSearch = (text: string) => {
setSearchTerm(text);
if (text) {
const normalizeText = lowerCase(text);
const matchedData = botUsers.filter(

View File

@ -193,6 +193,7 @@ const Ingestion: React.FC<IngestionProps> = ({
isNumberBasedPaging={!isEmpty(searchText)}
pipelineIdToFetchStatus={pipelineIdToFetchStatus}
pipelineType={pipelineType}
searchText={searchText}
serviceCategory={serviceCategory}
serviceName={serviceName}
triggerIngestion={triggerIngestion}

View File

@ -56,4 +56,5 @@ export interface IngestionListTableProps {
record: IngestionPipeline
) => ReactNode;
tableClassName?: string;
searchText?: string;
}

View File

@ -98,12 +98,21 @@ jest.mock('../../../../common/NextPrevious/NextPrevious', () =>
);
jest.mock('../../../../../utils/IngestionListTableUtils', () => ({
renderNameField: jest.fn().mockImplementation(() => <div>nameField</div>),
renderNameField: jest
.fn()
.mockImplementation(() => () => <div>nameField</div>),
renderScheduleField: jest
.fn()
.mockImplementation(() => <div>scheduleField</div>),
renderStatusField: jest.fn().mockImplementation(() => <div>statusField</div>),
renderTypeField: jest.fn().mockImplementation(() => <div>typeField</div>),
renderTypeField: jest
.fn()
.mockImplementation(() => () => <div>typeField</div>),
}));
jest.mock('../../../../../utils/EntityUtils', () => ({
...jest.requireActual('../../../../../utils/EntityUtils'),
highlightSearchText: jest.fn((text) => text),
}));
describe('Ingestion', () => {

View File

@ -33,7 +33,10 @@ import { UseAirflowStatusProps } from '../../../../../hooks/useAirflowStatus';
import { useApplicationStore } from '../../../../../hooks/useApplicationStore';
import { deleteIngestionPipelineById } from '../../../../../rest/ingestionPipelineAPI';
import { Transi18next } from '../../../../../utils/CommonUtils';
import { getEntityName } from '../../../../../utils/EntityUtils';
import {
getEntityName,
highlightSearchText,
} from '../../../../../utils/EntityUtils';
import {
renderNameField,
renderScheduleField,
@ -81,6 +84,7 @@ function IngestionListTable({
triggerIngestion,
customRenderNameField,
tableClassName,
searchText,
}: Readonly<IngestionListTableProps>) {
const { t } = useTranslation();
const { theme } = useApplicationStore();
@ -250,7 +254,7 @@ function IngestionListTable({
title: t('label.name'),
dataIndex: 'name',
key: 'name',
render: customRenderNameField ?? renderNameField,
render: customRenderNameField ?? renderNameField(searchText),
},
...(showDescriptionCol
? [
@ -261,7 +265,7 @@ function IngestionListTable({
render: (description: string) =>
!isUndefined(description) && description.trim() ? (
<RichTextEditorPreviewerV1
markdown={description}
markdown={highlightSearchText(description, searchText)}
maxLength={MAX_CHAR_LIMIT_ENTITY_SUMMARY}
/>
) : (
@ -280,7 +284,7 @@ function IngestionListTable({
dataIndex: 'pipelineType',
key: 'pipelineType',
width: 120,
render: renderTypeField,
render: renderTypeField(searchText),
},
]),
{

View File

@ -140,10 +140,21 @@ jest.mock('../../../rest/serviceAPI', () => ({
searchService: mockSearchService,
}));
jest.mock('../../../utils/EntityUtils', () => ({
getEntityName: jest.fn().mockReturnValue('Glue'),
jest.mock('../../../utils/StringsUtils', () => ({
...jest.requireActual('../../../utils/StringsUtils'),
stringToHTML: jest.fn((text) => text),
}));
jest.mock('../../../utils/EntityUtils', () => {
const actual = jest.requireActual('../../../utils/EntityUtils');
return {
...actual,
getEntityName: jest.fn().mockReturnValue('Glue'),
highlightSearchText: jest.fn((text) => text),
};
});
jest.mock('../../../utils/PermissionsUtils', () => ({
checkPermission: jest.fn().mockReturnValue(true),
}));

View File

@ -48,7 +48,7 @@ import { DatabaseServiceSearchSource } from '../../../interface/search.interface
import { ServicesType } from '../../../interface/service.interface';
import { getServices, searchService } from '../../../rest/serviceAPI';
import { getServiceLogo } from '../../../utils/CommonUtils';
import { getEntityName } from '../../../utils/EntityUtils';
import { getEntityName, highlightSearchText } from '../../../utils/EntityUtils';
import { checkPermission } from '../../../utils/PermissionsUtils';
import { getAddServicePath } from '../../../utils/RouterUtils';
import {
@ -56,6 +56,7 @@ import {
getResourceEntityFromServiceCategory,
getServiceTypesFromServiceCategory,
} from '../../../utils/ServiceUtils';
import { stringToHTML } from '../../../utils/StringsUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import { ListView } from '../../common/ListView/ListView.component';
@ -314,7 +315,9 @@ const Services = ({ serviceName }: ServicesProps) => {
record.fullyQualifiedName ?? record.name,
serviceName
)}>
{getEntityName(record)}
{stringToHTML(
highlightSearchText(getEntityName(record), searchTerm)
)}
</Link>
</div>
),
@ -329,7 +332,7 @@ const Services = ({ serviceName }: ServicesProps) => {
<RichTextEditorPreviewerV1
className="max-two-lines"
enableSeeMoreVariant={false}
markdown={description}
markdown={highlightSearchText(description, searchTerm)}
/>
) : (
<span className="text-grey-muted">{t('label.no-description')}</span>
@ -352,7 +355,9 @@ const Services = ({ serviceName }: ServicesProps) => {
filteredValue: serviceTypeFilter,
filters: serviceTypeFilters,
render: (serviceType) => (
<span className="font-normal text-grey-body">{serviceType}</span>
<span className="font-normal text-grey-body">
{stringToHTML(highlightSearchText(serviceType, searchTerm))}
</span>
),
},
{

View File

@ -707,6 +707,7 @@ const TeamDetailsV1 = ({
currentTeam={currentTeam}
data={childTeamList}
isFetchingAllTeamAdvancedDetails={isFetchingAllTeamAdvancedDetails}
searchTerm={searchTerm}
onTeamExpand={onTeamExpand}
/>
</Col>

View File

@ -51,10 +51,18 @@ jest.mock('../../../../rest/teamsAPI', () => ({
.mockImplementation(() => Promise.resolve(MOCK_CURRENT_TEAM)),
}));
jest.mock('../../../../utils/EntityUtils', () => ({
getEntityName: jest.fn().mockReturnValue('entityName'),
jest.mock('../../../../utils/StringsUtils', () => ({
...jest.requireActual('../../../../utils/StringsUtils'),
stringToHTML: jest.fn((text) => text),
}));
jest.mock('../../../../utils/EntityUtils', () => {
return {
getEntityName: jest.fn().mockReturnValue('entityName'),
highlightSearchText: jest.fn((text) => text),
};
});
jest.mock('../../../../utils/RouterUtils', () => ({
getTeamsWithFqnPath: jest.fn().mockReturnValue([]),
}));

View File

@ -33,8 +33,12 @@ import { Team } from '../../../../generated/entity/teams/team';
import { Include } from '../../../../generated/type/include';
import { getTeamByName, patchTeamDetail } from '../../../../rest/teamsAPI';
import { Transi18next } from '../../../../utils/CommonUtils';
import { getEntityName } from '../../../../utils/EntityUtils';
import {
getEntityName,
highlightSearchText,
} from '../../../../utils/EntityUtils';
import { getTeamsWithFqnPath } from '../../../../utils/RouterUtils';
import { stringToHTML } from '../../../../utils/StringsUtils';
import { getTableExpandableConfig } from '../../../../utils/TableUtils';
import { isDropRestricted } from '../../../../utils/TeamUtils';
import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils';
@ -50,6 +54,7 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
data,
onTeamExpand,
isFetchingAllTeamAdvancedDetails,
searchTerm,
}) => {
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
@ -68,7 +73,9 @@ const TeamHierarchy: FC<TeamHierarchyProps> = ({
<Link
className="link-hover"
to={getTeamsWithFqnPath(record.fullyQualifiedName || record.name)}>
{getEntityName(record)}
{stringToHTML(
highlightSearchText(getEntityName(record), searchTerm)
)}
</Link>
),
},

View File

@ -26,6 +26,7 @@ export interface TeamHierarchyProps {
updateChildNode?: boolean
) => void;
isFetchingAllTeamAdvancedDetails: boolean;
searchTerm?: string;
}
export interface MovedTeamProps {

View File

@ -10,12 +10,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ReactNode } from 'react';
import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface';
export interface DisplayNameProps {
id: string;
name?: string;
displayName?: string;
name?: ReactNode;
displayName?: ReactNode;
link: string;
onEditDisplayName?: (data: EntityName, id?: string) => Promise<void>;
allowRename?: boolean;

View File

@ -12,7 +12,7 @@
*/
import { Button, Tooltip, Typography } from 'antd';
import { AxiosError } from 'axios';
import { isEmpty } from 'lodash';
import { isEmpty, isString } from 'lodash';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
@ -85,8 +85,8 @@ const DisplayName: React.FC<DisplayNameProps> = ({
<EntityNameModal
allowRename={allowRename}
entity={{
name: name ?? '',
displayName,
name: isString(name) ? name : '',
displayName: isString(displayName) ? displayName : undefined,
}}
title={t('label.edit-entity', {
entity: t('label.display-name'),

View File

@ -43,6 +43,16 @@ jest.mock('../../../utils/ToastUtils', () => ({
showErrorToast: jest.fn(),
}));
jest.mock('../../../utils/StringsUtils', () => ({
...jest.requireActual('../../../utils/StringsUtils'),
stringToHTML: jest.fn((text) => text),
}));
jest.mock('../../../utils/EntityUtils', () => ({
...jest.requireActual('../../../utils/EntityUtils'),
highlightSearchText: jest.fn((text) => text),
}));
jest.mock(
'../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder',
() => {

View File

@ -27,7 +27,8 @@ import { Policy } from '../../../generated/entity/policies/policy';
import { Role } from '../../../generated/entity/teams/role';
import { EntityReference } from '../../../generated/type/entityReference';
import { getPolicies, getRoles } from '../../../rest/rolesAPIV1';
import { getEntityName } from '../../../utils/EntityUtils';
import { getEntityName, highlightSearchText } from '../../../utils/EntityUtils';
import { stringToHTML } from '../../../utils/StringsUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import './add-attribute-modal.less';
@ -55,6 +56,7 @@ const AddAttributeModal: FC<Props> = ({
const [searchedData, setSearchedData] = useState<EntityReference[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [selectedValues, setSelectedValues] = useState<string[]>(selectedKeys);
const [searchTerm, setSearchTerm] = useState<string>('');
const fetchPolicies = async () => {
setIsLoading(true);
@ -108,6 +110,7 @@ const AddAttributeModal: FC<Props> = ({
};
const handleSearch = (value: string) => {
setSearchTerm(value);
if (value) {
setSearchedData(
data.filter(
@ -178,10 +181,17 @@ const AddAttributeModal: FC<Props> = ({
gutter={[16, 16]}
key={option.id}
onClick={() => handleValueSelect(option.id)}>
<Col span={6}>{getEntityName(option)}</Col>
<Col span={6}>
{stringToHTML(
highlightSearchText(getEntityName(option), searchTerm)
)}
</Col>
<Col span={16}>
<RichTextEditorPreviewerV1
markdown={option.description || ''}
markdown={highlightSearchText(
option.description ?? '',
searchTerm
)}
/>
</Col>
<Col span={2}>

View File

@ -78,6 +78,16 @@ jest.mock('../SearchIndexFieldsTable/SearchIndexFieldsTable', () =>
)
);
jest.mock('../../../utils/StringsUtils', () => ({
...jest.requireActual('../../../utils/StringsUtils'),
stringToHTML: jest.fn((text) => text),
}));
jest.mock('../../../utils/EntityUtils', () => ({
...jest.requireActual('../../../utils/EntityUtils'),
highlightSearchText: jest.fn((text) => text),
}));
describe('SearchIndexFieldsTab component', () => {
it('SearchIndexFieldsTab should pass all the fields to SearchIndexFieldsTable when not searched anything', () => {
render(<SearchIndexFieldsTab {...mockProps} />);

View File

@ -128,6 +128,7 @@ function SearchIndexFieldsTab({
hasTagEditAccess={hasTagEditAccess}
isReadOnly={isReadOnly}
searchIndexFields={fields}
searchText={searchText}
searchedFields={searchedFields}
onThreadLinkSelect={onThreadLinkSelect}
onUpdate={onUpdate}

View File

@ -27,6 +27,7 @@ export interface SearchIndexFieldsTableProps {
entityFqn: string;
onUpdate: (fields: Array<SearchIndexField>) => Promise<void>;
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
searchText?: string;
}
export type SearchIndexCellRendered<T, K extends keyof T> = (

View File

@ -36,8 +36,14 @@ import { EntityType } from '../../../enums/entity.enum';
import { SearchIndexField } from '../../../generated/entity/data/searchIndex';
import { TagSource } from '../../../generated/type/schema';
import { TagLabel } from '../../../generated/type/tagLabel';
import { getColumnSorter, getEntityName } from '../../../utils/EntityUtils';
import {
getColumnSorter,
getEntityName,
highlightSearchArrayElement,
highlightSearchText,
} from '../../../utils/EntityUtils';
import { makeData } from '../../../utils/SearchIndexUtils';
import { stringToHTML } from '../../../utils/StringsUtils';
import {
getAllTags,
searchTagInData,
@ -62,6 +68,7 @@ const SearchIndexFieldsTable = ({
isReadOnly = false,
onThreadLinkSelect,
entityFqn,
searchText,
}: SearchIndexFieldsTableProps) => {
const { t } = useTranslation();
const [editField, setEditField] = useState<{
@ -148,7 +155,7 @@ const SearchIndexFieldsTable = ({
) : (
<Tooltip title={toLower(displayValue)}>
<Typography.Text ellipsis className="cursor-pointer">
{displayValue}
{highlightSearchArrayElement(displayValue, searchText)}
</Typography.Text>
</Tooltip>
)}
@ -166,7 +173,7 @@ const SearchIndexFieldsTable = ({
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
field: record.description,
field: highlightSearchText(record.description, searchText),
}}
entityFqn={entityFqn}
entityType={EntityType.SEARCH_INDEX}
@ -198,7 +205,11 @@ const SearchIndexFieldsTable = ({
sorter: getColumnSorter<SearchIndexField, 'name'>('name'),
render: (_, record: SearchIndexField) => (
<div className="d-inline-flex w-max-90">
<span className="break-word">{getEntityName(record)}</span>
<span className="break-word">
{stringToHTML(
highlightSearchText(getEntityName(record), searchText)
)}
</span>
</div>
),
},

View File

@ -0,0 +1,84 @@
/*
* Copyright 2025 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 { render } from '@testing-library/react';
import { PipelineType } from '../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { renderNameField, renderTypeField } from './IngestionListTableUtils';
jest.mock('./EntityUtils', () => ({
...jest.requireActual('./EntityUtils'),
highlightSearchText: jest.fn((text, searchText) => {
if (searchText) {
return text.replace(
new RegExp(searchText, 'gi'),
(match: string) =>
`<span data-highlight="true" class="text-highlighter">${match}</span>`
);
}
return text;
}),
}));
jest.mock('./StringsUtils', () => ({
...jest.requireActual('./StringsUtils'),
stringToHTML: jest.fn((text) => text),
}));
const mockRecord = {
name: 'Test Pipeline',
pipelineType: PipelineType.Metadata,
airflowConfig: {},
sourceConfig: {},
};
describe('renderNameField', () => {
it('should render the pipeline name with highlighted search text', () => {
const searchText = 'Test';
const { getByTestId } = render(renderNameField(searchText)('', mockRecord));
const pipelineNameElement = getByTestId('pipeline-name');
expect(pipelineNameElement.innerHTML).toBe(
'&lt;span data-highlight="true" class="text-highlighter"&gt;Test&lt;/span&gt; Pipeline'
);
});
it('should render the pipeline name without highlighting if searchText is not provided', () => {
const { getByTestId } = render(renderNameField()('', mockRecord));
const pipelineNameElement = getByTestId('pipeline-name');
expect(pipelineNameElement.innerHTML).toBe('Test Pipeline');
});
});
describe('renderTypeField', () => {
it('should render the pipeline type with highlighted search text', () => {
const searchText = 'metadata';
const { getByTestId } = render(renderTypeField(searchText)('', mockRecord));
const pipelineTypeElement = getByTestId('pipeline-type');
expect(pipelineTypeElement.innerHTML).toBe(
'&lt;span data-highlight="true" class="text-highlighter"&gt;metadata&lt;/span&gt;'
);
});
it('should render the pipeline type without highlighting if searchText is not provided', () => {
const { getByTestId } = render(renderTypeField()('', mockRecord));
const pipelineTypeElement = getByTestId('pipeline-type');
expect(pipelineTypeElement.innerHTML).toBe('metadata');
});
});

View File

@ -20,24 +20,29 @@ import { ReactComponent as TimeDateIcon } from '../assets/svg/time-date.svg';
import { NO_DATA_PLACEHOLDER } from '../constants/constants';
import { PIPELINE_INGESTION_RUN_STATUS } from '../constants/pipeline.constants';
import { IngestionPipeline } from '../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { getEntityName } from './EntityUtils';
import { getEntityName, highlightSearchText } from './EntityUtils';
import { getCurrentLocaleForConstrue } from './i18next/i18nextUtil';
import { stringToHTML } from './StringsUtils';
export const renderNameField = (_: string, record: IngestionPipeline) => (
<Typography.Text
className="m-b-0 d-block break-word"
data-testid="pipeline-name">
{getEntityName(record)}
</Typography.Text>
);
export const renderNameField =
(searchText?: string) => (_: string, record: IngestionPipeline) =>
(
<Typography.Text
className="m-b-0 d-block break-word"
data-testid="pipeline-name">
{stringToHTML(highlightSearchText(getEntityName(record), searchText))}
</Typography.Text>
);
export const renderTypeField = (_: string, record: IngestionPipeline) => (
<Typography.Text
className="m-b-0 d-block break-word"
data-testid="pipeline-type">
{record.pipelineType}
</Typography.Text>
);
export const renderTypeField =
(searchText?: string) => (_: string, record: IngestionPipeline) =>
(
<Typography.Text
className="m-b-0 d-block break-word"
data-testid="pipeline-type">
{stringToHTML(highlightSearchText(record.pipelineType, searchText))}
</Typography.Text>
);
export const renderStatusField = (_: string, record: IngestionPipeline) => (
<Tag