mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-29 09:42:23 +00:00
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:
parent
4a6c613c56
commit
7afecdd59c
@ -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}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -33,4 +33,5 @@ export interface ExploreSearchCardProps {
|
||||
hideBreadcrumbs?: boolean;
|
||||
actionPopoverContent?: React.ReactNode;
|
||||
onCheckboxChange?: (checked: boolean) => void;
|
||||
searchValue?: string;
|
||||
}
|
||||
|
||||
@ -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 : []}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -193,6 +193,7 @@ const Ingestion: React.FC<IngestionProps> = ({
|
||||
isNumberBasedPaging={!isEmpty(searchText)}
|
||||
pipelineIdToFetchStatus={pipelineIdToFetchStatus}
|
||||
pipelineType={pipelineType}
|
||||
searchText={searchText}
|
||||
serviceCategory={serviceCategory}
|
||||
serviceName={serviceName}
|
||||
triggerIngestion={triggerIngestion}
|
||||
|
||||
@ -56,4 +56,5 @@ export interface IngestionListTableProps {
|
||||
record: IngestionPipeline
|
||||
) => ReactNode;
|
||||
tableClassName?: string;
|
||||
searchText?: string;
|
||||
}
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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),
|
||||
},
|
||||
]),
|
||||
{
|
||||
|
||||
@ -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),
|
||||
}));
|
||||
|
||||
@ -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>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@ -707,6 +707,7 @@ const TeamDetailsV1 = ({
|
||||
currentTeam={currentTeam}
|
||||
data={childTeamList}
|
||||
isFetchingAllTeamAdvancedDetails={isFetchingAllTeamAdvancedDetails}
|
||||
searchTerm={searchTerm}
|
||||
onTeamExpand={onTeamExpand}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@ -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([]),
|
||||
}));
|
||||
|
||||
@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
@ -26,6 +26,7 @@ export interface TeamHierarchyProps {
|
||||
updateChildNode?: boolean
|
||||
) => void;
|
||||
isFetchingAllTeamAdvancedDetails: boolean;
|
||||
searchTerm?: string;
|
||||
}
|
||||
|
||||
export interface MovedTeamProps {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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',
|
||||
() => {
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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} />);
|
||||
|
||||
@ -128,6 +128,7 @@ function SearchIndexFieldsTab({
|
||||
hasTagEditAccess={hasTagEditAccess}
|
||||
isReadOnly={isReadOnly}
|
||||
searchIndexFields={fields}
|
||||
searchText={searchText}
|
||||
searchedFields={searchedFields}
|
||||
onThreadLinkSelect={onThreadLinkSelect}
|
||||
onUpdate={onUpdate}
|
||||
|
||||
@ -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> = (
|
||||
|
||||
@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
@ -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(
|
||||
'<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');
|
||||
});
|
||||
});
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user