diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.component.tsx index cc1f0df695b3..34fba24d6c80 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityHeaderTitle/EntityHeaderTitle.component.tsx @@ -55,7 +55,7 @@ const EntityHeaderTitle = ({ data-testid={`${serviceName}-${name}`} gutter={12} wrap={false}> - {icon && {icon}} + {icon && {icon}} ({ useApplicationStore: jest.fn(() => ({ theme: { primaryColor: '#fff' }, @@ -260,4 +267,33 @@ describe('Ingestion', () => { expect(deleteIngestionPipelineById).toHaveBeenCalledWith('id'); }); + + it('should fetch the permissions for all the ingestion pipelines', async () => { + (usePermissionProvider as jest.Mock).mockImplementation(() => ({ + getEntityPermissionByFqn: mockGetEntityPermissionByFqn, + })); + + await act(async () => { + render( + , + { + wrapper: MemoryRouter, + } + ); + }); + + expect(mockGetEntityPermissionByFqn).toHaveBeenNthCalledWith( + 1, + 'ingestionPipeline', + mockESIngestionData.fullyQualifiedName + ); + expect(mockGetEntityPermissionByFqn).toHaveBeenNthCalledWith( + 2, + 'ingestionPipeline', + mockIngestionData.fullyQualifiedName + ); + }); }); 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 86738d11dc52..c26a0064b373 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 @@ -157,7 +157,10 @@ function IngestionListTable({ const fetchIngestionPipelinesPermission = useCallback(async () => { try { const promises = ingestionData.map((item) => - getEntityPermissionByFqn(ResourceEntity.INGESTION_PIPELINE, item.name) + getEntityPermissionByFqn( + ResourceEntity.INGESTION_PIPELINE, + item.fullyQualifiedName ?? '' + ) ); const response = await Promise.allSettled(promises); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/PipelineActions.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActions.interface.ts similarity index 72% rename from openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/PipelineActions.interface.ts rename to openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActions.interface.ts index 0ea302cd0061..075361568eec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/PipelineActions.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActions.interface.ts @@ -11,10 +11,11 @@ * limitations under the License. */ -import { IngestionServicePermission } from '../../../../context/PermissionProvider/PermissionProvider.interface'; -import { ServiceCategory } from '../../../../enums/service.enum'; -import { IngestionPipeline } from '../../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; -import { SelectedRowDetails } from './ingestion.interface'; +import { ButtonProps } from 'antd'; +import { IngestionServicePermission } from '../../../../../../context/PermissionProvider/PermissionProvider.interface'; +import { ServiceCategory } from '../../../../../../enums/service.enum'; +import { IngestionPipeline } from '../../../../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; +import { SelectedRowDetails } from '../../ingestion.interface'; export interface PipelineActionsProps { pipeline: IngestionPipeline; @@ -28,4 +29,5 @@ export interface PipelineActionsProps { handleEnableDisableIngestion?: (id: string) => Promise; handleIsConfirmationModalOpen: (value: boolean) => void; onIngestionWorkflowsUpdate?: () => void; + moreActionButtonProps?: ButtonProps; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActions.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActions.tsx index 90651e429845..2aab18773b56 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActions.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActions.tsx @@ -22,8 +22,8 @@ import { Operation } from '../../../../../../generated/entity/policies/accessCon import { PipelineType } from '../../../../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { getLoadingStatus } from '../../../../../../utils/CommonUtils'; import { getLogsViewerPath } from '../../../../../../utils/RouterUtils'; -import { PipelineActionsProps } from '../../PipelineActions.interface'; import './pipeline-actions.less'; +import { PipelineActionsProps } from './PipelineActions.interface'; import PipelineActionsDropdown from './PipelineActionsDropdown'; function PipelineActions({ @@ -38,6 +38,7 @@ function PipelineActions({ handleIsConfirmationModalOpen, onIngestionWorkflowsUpdate, handleEditClick, + moreActionButtonProps, }: Readonly) { const history = useHistory(); const { t } = useTranslation(); @@ -168,6 +169,7 @@ function PipelineActions({ handleIsConfirmationModalOpen={handleIsConfirmationModalOpen} ingestion={pipeline} ingestionPipelinePermissions={ingestionPipelinePermissions} + moreActionButtonProps={moreActionButtonProps} serviceCategory={serviceCategory} serviceName={serviceName} triggerIngestion={triggerIngestion} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.interface.ts index a572a7b4c3c5..b38d19a6ba8a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.interface.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { ButtonProps } from 'antd'; import { IngestionServicePermission } from '../../../../../../context/PermissionProvider/PermissionProvider.interface'; import { IngestionPipeline } from '../../../../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { SelectedRowDetails } from '../../ingestion.interface'; @@ -26,4 +27,5 @@ export interface PipelineActionsDropdownProps { handleDeleteSelection: (row: SelectedRowDetails) => void; handleIsConfirmationModalOpen: (value: boolean) => void; onIngestionWorkflowsUpdate?: () => void; + moreActionButtonProps?: ButtonProps; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.test.tsx index 708d91ae94db..93834477741a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.test.tsx @@ -324,4 +324,26 @@ describe('PipelineActionsDropdown', () => { expect(screen.queryByText('KillIngestionPipelineModal')).toBeNull(); }); + + it('should pass the moreActionButtonProps to the more action button', async () => { + const mockOnClick = jest.fn(); + + await act(async () => { + render( + , + { + wrapper: MemoryRouter, + } + ); + }); + + await clickOnMoreActions(); + + expect(mockOnClick).toHaveBeenCalled(); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.tsx index 61baee49f856..a6b5db9165ec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.tsx @@ -50,6 +50,7 @@ function PipelineActionsDropdown({ handleIsConfirmationModalOpen, onIngestionWorkflowsUpdate, ingestionPipelinePermissions, + moreActionButtonProps, }: Readonly) { const history = useHistory(); const { t } = useTranslation(); @@ -270,6 +271,7 @@ function PipelineActionsDropdown({ icon={} type="link" onClick={() => setIsOpen((value) => !value)} + {...moreActionButtonProps} /> {isKillModalOpen && selectedPipeline && id === selectedPipeline?.id && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/SuccessScreen/SuccessScreen.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/SuccessScreen/SuccessScreen.tsx index 3f9bc9ce950a..d5c554a45dcb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/SuccessScreen/SuccessScreen.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/SuccessScreen/SuccessScreen.tsx @@ -101,10 +101,7 @@ const SuccessScreen = ({ - + {isUndefined(successMessage) ? ( diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/IngestionListTable.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/IngestionListTable.mock.ts index e0ae76f1857f..9e3a3865f529 100644 --- a/openmetadata-ui/src/main/resources/ui/src/mocks/IngestionListTable.mock.ts +++ b/openmetadata-ui/src/main/resources/ui/src/mocks/IngestionListTable.mock.ts @@ -13,8 +13,8 @@ import { AddIngestionButtonProps } from '../components/Settings/Services/Ingestion/AddIngestionButton.interface'; import { IngestionListTableProps } from '../components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.interface'; +import { PipelineActionsProps } from '../components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActions.interface'; import { PipelineActionsDropdownProps } from '../components/Settings/Services/Ingestion/IngestionListTable/PipelineActions/PipelineActionsDropdown.interface'; -import { PipelineActionsProps } from '../components/Settings/Services/Ingestion/PipelineActions.interface'; import { ServiceCategory } from '../enums/service.enum'; import { DatabaseServiceType } from '../generated/entity/data/database'; import { ConfigType } from '../generated/entity/services/databaseService'; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.test.ts index f9fee0938250..28f31633bc5e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.test.ts @@ -11,7 +11,10 @@ * limitations under the License. */ import { EntityFields } from '../enums/AdvancedSearch.enum'; -import { AdvancedSearchClassBase } from './AdvancedSearchClassBase'; +import { SearchIndex } from '../enums/search.enum'; +import advancedSearchClassBase, { + AdvancedSearchClassBase, +} from './AdvancedSearchClassBase'; jest.mock('../rest/miscAPI', () => ({ getAggregateFieldOptions: jest.fn().mockImplementation(() => @@ -50,3 +53,134 @@ describe('AdvancedSearchClassBase', () => { ]); }); }); + +describe('getEntitySpecificQueryBuilderFields', () => { + it('should return table specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.TABLE, + ]); + + expect(Object.keys(result)).toEqual([ + EntityFields.DATABASE, + EntityFields.DATABASE_SCHEMA, + EntityFields.TABLE_TYPE, + EntityFields.COLUMN_DESCRIPTION_STATUS, + ]); + }); + + it('should return pipeline specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.PIPELINE, + ]); + + expect(Object.keys(result)).toEqual([EntityFields.TASK]); + }); + + it('should return dashboard specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.DASHBOARD, + ]); + + expect(Object.keys(result)).toEqual([ + EntityFields.DATA_MODEL, + EntityFields.CHART, + EntityFields.PROJECT, + ]); + }); + + it('should return topic specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.TOPIC, + ]); + + expect(Object.keys(result)).toEqual([EntityFields.SCHEMA_FIELD]); + }); + + it('should return mlModel specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.MLMODEL, + ]); + + expect(Object.keys(result)).toEqual([EntityFields.FEATURE]); + }); + + it('should return container specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.CONTAINER, + ]); + + expect(Object.keys(result)).toEqual([EntityFields.CONTAINER_COLUMN]); + }); + + it('should return searchIndex specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.SEARCH_INDEX, + ]); + + expect(Object.keys(result)).toEqual([EntityFields.FIELD]); + }); + + it('should return dataModel specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.DASHBOARD_DATA_MODEL, + ]); + + expect(Object.keys(result)).toEqual([ + EntityFields.DATA_MODEL_TYPE, + EntityFields.PROJECT, + ]); + }); + + it('should return apiEndpoint specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.API_ENDPOINT_INDEX, + ]); + + expect(Object.keys(result)).toEqual([ + EntityFields.REQUEST_SCHEMA_FIELD, + EntityFields.RESPONSE_SCHEMA_FIELD, + ]); + }); + + it('should return glossary specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.GLOSSARY_TERM, + ]); + + expect(Object.keys(result)).toEqual([EntityFields.GLOSSARY_TERM_STATUS]); + }); + + it('should return databaseSchema specific fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.DATABASE_SCHEMA, + ]); + + expect(Object.keys(result)).toEqual([EntityFields.DATABASE]); + }); + + it('should return empty fields for multiple indices with no common fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.TABLE, + SearchIndex.PIPELINE, + ]); + + expect(Object.keys(result)).toEqual([]); + }); + + it('should return combined fields for multiple indices with common fields', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + SearchIndex.TABLE, + SearchIndex.DATABASE_SCHEMA, + ]); + + expect(Object.keys(result)).toEqual([EntityFields.DATABASE]); + }); + + it('should return empty object for unknown index', () => { + const result = advancedSearchClassBase.getEntitySpecificQueryBuilderFields([ + 'UNKNOWN_INDEX' as SearchIndex, + ]); + + expect(Object.keys(result)).toEqual([]); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts index 05da5daaffd5..cc87cc631433 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchClassBase.ts @@ -143,6 +143,24 @@ class AdvancedSearchClassBase { }; }; + /** + * Fields specific to database schema + */ + databaseSchemaQueryBuilderFields: Fields = { + [EntityFields.DATABASE]: { + label: t('label.database'), + type: 'select', + mainWidgetProps: this.mainWidgetProps, + fieldSettings: { + asyncFetch: this.autocomplete({ + searchIndex: SearchIndex.DATABASE_SCHEMA, + entityField: EntityFields.DATABASE, + }), + useAsyncSearch: true, + }, + }, + }; + /** * Fields specific to tables */ @@ -477,7 +495,7 @@ class AdvancedSearchClassBase { mainWidgetProps: this.mainWidgetProps, fieldSettings: { asyncFetch: this.autocomplete({ - searchIndex: SearchIndex.DATA_ASSET, + searchIndex: entitySearchIndex, entityField: EntityFields.DISPLAY_NAME_KEYWORD, }), useAsyncSearch: true, @@ -498,7 +516,7 @@ class AdvancedSearchClassBase { mainWidgetProps: this.mainWidgetProps, fieldSettings: { asyncFetch: this.autocomplete({ - searchIndex: SearchIndex.DATA_ASSET, + searchIndex: entitySearchIndex, entityField: EntityFields.NAME_KEYWORD, }), useAsyncSearch: true, @@ -674,6 +692,7 @@ class AdvancedSearchClassBase { [SearchIndex.DASHBOARD_DATA_MODEL]: this.dataModelQueryBuilderFields, [SearchIndex.API_ENDPOINT_INDEX]: this.apiEndpointQueryBuilderFields, [SearchIndex.GLOSSARY_TERM]: this.glossaryQueryBuilderFields, + [SearchIndex.DATABASE_SCHEMA]: this.databaseSchemaQueryBuilderFields, [SearchIndex.ALL]: { ...this.tableQueryBuilderFields, ...this.pipelineQueryBuilderFields, @@ -699,9 +718,29 @@ class AdvancedSearchClassBase { }, }; - entitySearchIndex.forEach((index) => { - configs = { ...configs, ...(configIndexMapping[index] ?? {}) }; - }); + // Find out the common fields between the selected indices + if (!isEmpty(entitySearchIndex)) { + const firstIndex = entitySearchIndex[0]; + + // Fields config for the first index + configs = { ...configIndexMapping[firstIndex] }; + + // Iterate over the rest of the indices to see the common fields + entitySearchIndex.slice(1).forEach((index) => { + // Get the current config for the current iteration index + const currentConfig = configIndexMapping[index] ?? {}; + + // Filter out the fields that are not common between the current and previous configs + configs = Object.keys(configs).reduce((acc, key) => { + // If the key exists in the current config, add it to the accumulator + if (currentConfig[key]) { + acc[key] = configs[key]; + } + + return acc; + }, {} as Fields); + }); + } return configs; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx index 9e50c736e007..86b84c97da2e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/IngestionUtils.tsx @@ -359,7 +359,7 @@ export const getSuccessMessage = ( return ( - {`"${ingestionName}"`} + {`"${ingestionName}"`} {status === FormSubmitType.ADD ? createMessage : updateMessage}