From 7afecdd59cce2837e2360a88fcfa60fcc533fafb Mon Sep 17 00:00:00 2001 From: Pranita Fulsundar Date: Thu, 30 Jan 2025 12:03:22 +0530 Subject: [PATCH] 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 --- .../DatabaseSchemaTable.tsx | 12 ++- .../ExploreSearchCard.interface.ts | 1 + .../ExploreSearchCard/ExploreSearchCard.tsx | 22 ++++- .../tabs/AssetsTabs.component.tsx | 1 + .../BotListV1/BotListV1.component.test.tsx | 11 +++ .../Bot/BotListV1/BotListV1.component.tsx | 17 +++- .../Ingestion/Ingestion.component.tsx | 1 + .../IngestionListTable.interface.ts | 1 + .../IngestionListTable.test.tsx | 13 ++- .../IngestionListTable/IngestionListTable.tsx | 12 ++- .../Settings/Services/Services.test.tsx | 15 +++- .../components/Settings/Services/Services.tsx | 13 ++- .../Team/TeamDetails/TeamDetailsV1.tsx | 1 + .../Team/TeamDetails/TeamHierarchy.test.tsx | 12 ++- .../Team/TeamDetails/TeamHierarchy.tsx | 11 ++- .../Team/TeamDetails/team.interface.ts | 1 + .../DisplayName/DisplayName.interface.ts | 5 +- .../common/DisplayName/DisplayName.tsx | 6 +- .../AddAttributeModal.test.tsx | 10 +++ .../AddAttributeModal/AddAttributeModal.tsx | 16 +++- .../SearchIndexFieldsTab.test.tsx | 10 +++ .../SearchIndexFieldsTab.tsx | 1 + .../SearchIndexFieldsTable.interface.ts | 1 + .../SearchIndexFieldsTable.tsx | 19 ++++- .../utils/IngestionListTableUtils.test.tsx | 84 +++++++++++++++++++ .../ui/src/utils/IngestionListTableUtils.tsx | 35 ++++---- 26 files changed, 278 insertions(+), 53 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.test.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx index 74575482296..797874d3861 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/DatabaseSchema/DatabaseSchemaTable/DatabaseSchemaTable.tsx @@ -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) => ( ), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.interface.ts index 1dba6ed6473..e6b024b9821 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.interface.ts @@ -33,4 +33,5 @@ export interface ExploreSearchCardProps { hideBreadcrumbs?: boolean; actionPopoverContent?: React.ReactNode; onCheckboxChange?: (checked: boolean) => void; + searchValue?: string; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx index 2a61c7a3815..8a81e86dbb4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx @@ -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 = forwardRef< showCheckboxes = false, checked = false, onCheckboxChange, + searchValue, }, ref ) => { @@ -222,7 +223,12 @@ const ExploreSearchCard: React.FC = forwardRef< - {stringToHTML(searchClassBase.getEntityName(source))} + {stringToHTML( + highlightSearchText( + searchClassBase.getEntityName(source), + searchValue + ) + )} ) : ( @@ -250,7 +256,12 @@ const ExploreSearchCard: React.FC = forwardRef< - {stringToHTML(searchClassBase.getEntityName(source))} + {stringToHTML( + highlightSearchText( + searchClassBase.getEntityName(source), + searchValue + ) + )} @@ -296,7 +307,10 @@ const ExploreSearchCard: React.FC = forwardRef<
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.component.tsx index 579be93c4fd..c4dc7233e22 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.component.tsx @@ -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} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.test.tsx index 739c1eb6678..b3e9f1af63b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.test.tsx @@ -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(, { wrapper: MemoryRouter }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 7c6b402f7cb..92e57ac7284 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -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([]); + const [searchTerm, setSearchTerm] = useState(''); const breadcrumbs: TitleBreadcrumbProps['titleLinks'] = useMemo( () => getSettingPageEntityBreadCrumb(GlobalSettingsMenuCategory.BOTS), @@ -123,7 +128,7 @@ const BotListV1 = ({ return ( - {name} + {stringToHTML(highlightSearchText(name, searchTerm))} ); }, @@ -134,7 +139,12 @@ const BotListV1 = ({ key: 'description', render: (_, record) => record?.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( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.component.tsx index 0bb51802490..03b4cfeea04 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.component.tsx @@ -193,6 +193,7 @@ const Ingestion: React.FC = ({ isNumberBasedPaging={!isEmpty(searchText)} pipelineIdToFetchStatus={pipelineIdToFetchStatus} pipelineType={pipelineType} + searchText={searchText} serviceCategory={serviceCategory} serviceName={serviceName} triggerIngestion={triggerIngestion} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.interface.ts index e059b0f8fd2..94514af804d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.interface.ts @@ -56,4 +56,5 @@ export interface IngestionListTableProps { record: IngestionPipeline ) => ReactNode; tableClassName?: string; + searchText?: string; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.test.tsx index 8fb97ca21f4..e2480974b67 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.test.tsx @@ -98,12 +98,21 @@ jest.mock('../../../../common/NextPrevious/NextPrevious', () => ); jest.mock('../../../../../utils/IngestionListTableUtils', () => ({ - renderNameField: jest.fn().mockImplementation(() =>
nameField
), + renderNameField: jest + .fn() + .mockImplementation(() => () =>
nameField
), renderScheduleField: jest .fn() .mockImplementation(() =>
scheduleField
), renderStatusField: jest.fn().mockImplementation(() =>
statusField
), - renderTypeField: jest.fn().mockImplementation(() =>
typeField
), + renderTypeField: jest + .fn() + .mockImplementation(() => () =>
typeField
), +})); + +jest.mock('../../../../../utils/EntityUtils', () => ({ + ...jest.requireActual('../../../../../utils/EntityUtils'), + highlightSearchText: jest.fn((text) => text), })); describe('Ingestion', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.tsx index c0123163103..7ca82cdc2bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.tsx @@ -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) { 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() ? ( ) : ( @@ -280,7 +284,7 @@ function IngestionListTable({ dataIndex: 'pipelineType', key: 'pipelineType', width: 120, - render: renderTypeField, + render: renderTypeField(searchText), }, ]), { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx index 0eab37f464e..dc2821bd022 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx @@ -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), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx index a6df62acd5f..b199e98c6b3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx @@ -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) + )}
), @@ -329,7 +332,7 @@ const Services = ({ serviceName }: ServicesProps) => { ) : ( {t('label.no-description')} @@ -352,7 +355,9 @@ const Services = ({ serviceName }: ServicesProps) => { filteredValue: serviceTypeFilter, filters: serviceTypeFilters, render: (serviceType) => ( - {serviceType} + + {stringToHTML(highlightSearchText(serviceType, searchTerm))} + ), }, { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx index 680d6e73222..66ed0664ee2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx @@ -707,6 +707,7 @@ const TeamDetailsV1 = ({ currentTeam={currentTeam} data={childTeamList} isFetchingAllTeamAdvancedDetails={isFetchingAllTeamAdvancedDetails} + searchTerm={searchTerm} onTeamExpand={onTeamExpand} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.test.tsx index 66a47d8015e..6bbacdb5c8c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.test.tsx @@ -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([]), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx index 16c98178064..6364816795d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamHierarchy.tsx @@ -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 = ({ data, onTeamExpand, isFetchingAllTeamAdvancedDetails, + searchTerm, }) => { const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); @@ -68,7 +73,9 @@ const TeamHierarchy: FC = ({ - {getEntityName(record)} + {stringToHTML( + highlightSearchText(getEntityName(record), searchTerm) + )} ), }, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/team.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/team.interface.ts index ee530c5dc1e..92bd74af0c1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/team.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/team.interface.ts @@ -26,6 +26,7 @@ export interface TeamHierarchyProps { updateChildNode?: boolean ) => void; isFetchingAllTeamAdvancedDetails: boolean; + searchTerm?: string; } export interface MovedTeamProps { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.interface.ts index b7a3f0f8218..20e04e0d293 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.interface.ts @@ -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; allowRename?: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx index 5a8a1f23a2a..ea59ef13593 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DisplayName/DisplayName.tsx @@ -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 = ({ ({ 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', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddAttributeModal/AddAttributeModal.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddAttributeModal/AddAttributeModal.tsx index bc7679846d8..314ff889600 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddAttributeModal/AddAttributeModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddAttributeModal/AddAttributeModal.tsx @@ -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 = ({ const [searchedData, setSearchedData] = useState([]); const [isLoading, setIsLoading] = useState(false); const [selectedValues, setSelectedValues] = useState(selectedKeys); + const [searchTerm, setSearchTerm] = useState(''); const fetchPolicies = async () => { setIsLoading(true); @@ -108,6 +110,7 @@ const AddAttributeModal: FC = ({ }; const handleSearch = (value: string) => { + setSearchTerm(value); if (value) { setSearchedData( data.filter( @@ -178,10 +181,17 @@ const AddAttributeModal: FC = ({ gutter={[16, 16]} key={option.id} onClick={() => handleValueSelect(option.id)}> - {getEntityName(option)} + + {stringToHTML( + highlightSearchText(getEntityName(option), searchTerm) + )} + diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx index b9ca08de45f..7f057d3e4e2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.test.tsx @@ -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(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx index 615e37edd93..ae7858e75a4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTab/SearchIndexFieldsTab.tsx @@ -128,6 +128,7 @@ function SearchIndexFieldsTab({ hasTagEditAccess={hasTagEditAccess} isReadOnly={isReadOnly} searchIndexFields={fields} + searchText={searchText} searchedFields={searchedFields} onThreadLinkSelect={onThreadLinkSelect} onUpdate={onUpdate} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.interface.ts b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.interface.ts index ce56c2cea92..08d6565271e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.interface.ts @@ -27,6 +27,7 @@ export interface SearchIndexFieldsTableProps { entityFqn: string; onUpdate: (fields: Array) => Promise; onThreadLinkSelect: (value: string, threadType?: ThreadType) => void; + searchText?: string; } export type SearchIndexCellRendered = ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.tsx index c36e89465c7..89c3dac8400 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexFieldsTable/SearchIndexFieldsTable.tsx @@ -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 = ({ ) : ( - {displayValue} + {highlightSearchArrayElement(displayValue, searchText)} )} @@ -166,7 +173,7 @@ const SearchIndexFieldsTable = ({ ('name'), render: (_, record: SearchIndexField) => (
- {getEntityName(record)} + + {stringToHTML( + highlightSearchText(getEntityName(record), searchText) + )} +
), }, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.test.tsx new file mode 100644 index 00000000000..faa1b10d867 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.test.tsx @@ -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) => + `${match}` + ); + } + + 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( + '<span data-highlight="true" class="text-highlighter">Test</span> 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( + '<span data-highlight="true" class="text-highlighter">metadata</span>' + ); + }); + + 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'); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.tsx index f70fb35c800..c8727f71129 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionListTableUtils.tsx @@ -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) => ( - - {getEntityName(record)} - -); +export const renderNameField = + (searchText?: string) => (_: string, record: IngestionPipeline) => + ( + + {stringToHTML(highlightSearchText(getEntityName(record), searchText))} + + ); -export const renderTypeField = (_: string, record: IngestionPipeline) => ( - - {record.pipelineType} - -); +export const renderTypeField = + (searchText?: string) => (_: string, record: IngestionPipeline) => + ( + + {stringToHTML(highlightSearchText(record.pipelineType, searchText))} + + ); export const renderStatusField = (_: string, record: IngestionPipeline) => (