From a95460525c12a9d92c4fb51550a23907131b35a3 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 7 Apr 2026 23:07:20 +0530 Subject: [PATCH 1/6] fix: action buttons visible immediately despite slow pipelineStatus API --- .../e2e/nightly/ServiceIngestion.spec.ts | 156 ++++++++++++++++++ .../IngestionListTable/IngestionListTable.tsx | 84 +++++----- 2 files changed, 197 insertions(+), 43 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/nightly/ServiceIngestion.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/nightly/ServiceIngestion.spec.ts index 95fbbab1445f..3ffb91e1033f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/nightly/ServiceIngestion.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/nightly/ServiceIngestion.spec.ts @@ -386,3 +386,159 @@ test.describe.serial( }); } ); + +const slowPipelineService = new MysqlIngestionClass({ + shouldTestConnection: false, + shouldAddIngestion: false, +}); +let slowTestPipeline: { + id: string; + name: string; + fullyQualifiedName: string; +}; + +test.describe.serial( + 'Action buttons visible despite slow pipelineStatus API', + PLAYWRIGHT_INGESTION_TAG_OBJ, + () => { + test.beforeEach('Navigate to database services', async ({ page }) => { + await redirectToHomePage(page); + await settingClick( + page, + slowPipelineService.category as unknown as SettingOptionsType + ); + }); + + test('Setup: create MySQL service and ingestion pipeline', async ({ + page, + }) => { + await slowPipelineService.createService(page); + + const { apiContext } = await getApiContext(page); + + const serviceResponse = await apiContext + .get( + `/api/v1/services/databaseServices/name/${encodeURIComponent( + slowPipelineService.getServiceName() + )}` + ) + .then((res) => res.json()); + + const createPipelineResponse = await apiContext.post( + '/api/v1/services/ingestionPipelines', + { + data: { + airflowConfig: {}, + loggerLevel: 'INFO', + name: `${slowPipelineService.getServiceName()}-metadata`, + pipelineType: 'metadata', + service: { + id: serviceResponse.id, + type: 'databaseService', + }, + sourceConfig: { + config: { + type: 'DatabaseMetadata', + }, + }, + }, + } + ); + + expect(createPipelineResponse.status()).toBe(201); + const createdPipeline = await createPipelineResponse.json(); + + await apiContext.post( + `/api/v1/services/ingestionPipelines/deploy/${createdPipeline.id}` + ); + + slowTestPipeline = { + id: createdPipeline.id, + name: createdPipeline.name, + fullyQualifiedName: createdPipeline.fullyQualifiedName, + }; + }); + + /** + * Validates that action buttons (logs, pause, run) are visible and functional + * even when the pipelineStatus API response is delayed (simulated via route mock). + * + * Regression test for the issue where high pipelineStatus API latency blocked + * rendering of action icons and the pause/resume button until the slow API resolved. + */ + test('Action buttons and pause visible when pipelineStatus API is slow', async ({ + page, + }) => { + test.slow(); + + // Mock the pipelineStatus endpoint to simulate high latency + await page.route( + `**/api/v1/services/ingestionPipelines/${encodeURIComponent( + slowTestPipeline.fullyQualifiedName + )}/pipelineStatus**`, + async (route) => { + await page.waitForTimeout(8000); + await route.continue(); + } + ); + + await visitServiceDetailsPage( + page, + { + type: slowPipelineService.category, + name: slowPipelineService.getServiceName(), + }, + false, + false + ); + + await page.getByTestId('data-assets-header').waitFor(); + await page.getByTestId('agents').click(); + + const metadataTab = page.locator('[data-testid="metadata-sub-tab"]'); + if (await metadataTab.isVisible()) { + await metadataTab.click(); + } + + const pipelineRow = page.locator( + `[data-row-key*="${slowTestPipeline.name}"]` + ); + + await expect(pipelineRow).toBeVisible({ timeout: 15000 }); + + // Action buttons must be visible immediately — before the slow pipelineStatus + // API resolves — verifying permissions don't wait on run history + await expect(pipelineRow.getByTestId('pause-button')).toBeVisible({ + timeout: 5000, + }); + + await expect(pipelineRow.getByTestId('logs-button')).toBeVisible({ + timeout: 5000, + }); + + await expect(pipelineRow.getByTestId('more-actions')).toBeVisible({ + timeout: 5000, + }); + + // Open the more-actions dropdown and verify the run button is present + await pipelineRow.getByTestId('more-actions').click(); + await expect(page.getByTestId('run-button')).toBeVisible({ + timeout: 5000, + }); + + // Trigger a pipeline run via the run button + const triggerResponse = page.waitForResponse( + (res) => + res.url().includes('/services/ingestionPipelines/trigger/') && + res.request().method() === 'POST' + ); + await page.getByTestId('run-button').click(); + await triggerResponse; + + // Verify the run was triggered by checking the pipeline row shows a running state + await expect( + pipelineRow.getByTestId('pipeline-status').first() + ).toBeVisible({ timeout: 15000 }); + }); + } +); 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 03259271c93d..13427d39a3ac 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 @@ -186,23 +186,22 @@ function IngestionListTable({ [handleCancelConfirmationModal] ); - const fetchIngestionPipelineExtraDetails = useCallback(async () => { - try { - setIsIngestionRunsLoading(true); - const permissionPromises = ingestionData.map((item) => - getEntityPermissionByFqn( - ResourceEntity.INGESTION_PIPELINE, - item.fullyQualifiedName ?? '' - ) - ); - const recentRunStatusPromises = ingestionData.map((item) => - getRunHistoryForPipeline(item.fullyQualifiedName ?? '', { limit: 5 }) - ); - const permissionResponse = await Promise.allSettled(permissionPromises); - const recentRunStatusResponse = await Promise.allSettled( - recentRunStatusPromises - ); + const fetchIngestionPipelineExtraDetails = useCallback(() => { + setIsIngestionRunsLoading(true); + + const permissionPromises = ingestionData.map((item) => + getEntityPermissionByFqn( + ResourceEntity.INGESTION_PIPELINE, + item.fullyQualifiedName ?? '' + ) + ); + const recentRunStatusPromises = ingestionData.map((item) => + getRunHistoryForPipeline(item.fullyQualifiedName ?? '', { limit: 5 }) + ); + + // Fire both batches concurrently — whichever settles first updates state immediately + Promise.allSettled(permissionPromises).then((permissionResponse) => { const permissionData = permissionResponse.reduce((acc, cv, index) => { return { ...acc, @@ -210,36 +209,35 @@ function IngestionListTable({ cv.status === 'fulfilled' ? cv.value : {}, }; }, {}); + setIngestionPipelinePermissions(permissionData); + }); - const recentRunStatusData = recentRunStatusResponse.reduce( - (acc, cv, index) => { - let value: PipelineStatus[] = []; - - if (cv.status === 'fulfilled') { - const runs = cv.value.data ?? []; - - const ingestion = ingestionData[index]; + Promise.allSettled(recentRunStatusPromises) + .then((recentRunStatusResponse) => { + const recentRunStatusData = recentRunStatusResponse.reduce( + (acc, cv, index) => { + let value: PipelineStatus[] = []; - value = - runs.length === 0 && ingestion?.pipelineStatuses - ? [ingestion.pipelineStatuses] - : runs; - } + if (cv.status === 'fulfilled') { + const runs = cv.value.data ?? []; + const ingestion = ingestionData[index]; + value = + runs.length === 0 && ingestion?.pipelineStatuses + ? [ingestion.pipelineStatuses] + : runs; + } - return { - ...acc, - [ingestionData?.[index].name]: value, - }; - }, - {} - ); - setIngestionPipelinePermissions(permissionData); - setRecentRunStatuses(recentRunStatusData); - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setIsIngestionRunsLoading(false); - } + return { + ...acc, + [ingestionData?.[index].name]: value, + }; + }, + {} + ); + setRecentRunStatuses(recentRunStatusData); + }) + .catch((error) => showErrorToast(error as AxiosError)) + .finally(() => setIsIngestionRunsLoading(false)); }, [ingestionData]); const { isFetchingStatus, platform } = useMemo( From 6830e315c5ce07be1e16cba37f422acb0a1acff9 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 7 Apr 2026 23:31:23 +0530 Subject: [PATCH 2/6] fixed the recent run overlapping issue --- .../Ingestion/IngestionListTable/IngestionListTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 13427d39a3ac..e1dad5e49323 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 @@ -377,7 +377,7 @@ function IngestionListTable({ title: t('label.recent-run-plural'), dataIndex: 'recentRuns', key: 'recentRuns', - width: 150, + width: 180, render: (_: string, record: IngestionPipeline) => ( Date: Tue, 7 Apr 2026 23:36:32 +0530 Subject: [PATCH 3/6] addressed gitar comment --- .../IngestionListTable/IngestionListTable.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) 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 e1dad5e49323..a43183854d4d 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 @@ -201,16 +201,18 @@ function IngestionListTable({ ); // Fire both batches concurrently — whichever settles first updates state immediately - Promise.allSettled(permissionPromises).then((permissionResponse) => { - const permissionData = permissionResponse.reduce((acc, cv, index) => { - return { - ...acc, - [ingestionData?.[index].name]: - cv.status === 'fulfilled' ? cv.value : {}, - }; - }, {}); - setIngestionPipelinePermissions(permissionData); - }); + Promise.allSettled(permissionPromises) + .then((permissionResponse) => { + const permissionData = permissionResponse.reduce((acc, cv, index) => { + return { + ...acc, + [ingestionData?.[index].name]: + cv.status === 'fulfilled' ? cv.value : {}, + }; + }, {}); + setIngestionPipelinePermissions(permissionData); + }) + .catch((error) => showErrorToast(error as AxiosError)); Promise.allSettled(recentRunStatusPromises) .then((recentRunStatusResponse) => { From 36961cea8c1d874c650d68467ceacccfe2b8f6fa Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Fri, 10 Apr 2026 23:01:08 +0530 Subject: [PATCH 4/6] addressed PR comment --- .../e2e/nightly/ServiceIngestion.spec.ts | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/nightly/ServiceIngestion.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/nightly/ServiceIngestion.spec.ts index 3ffb91e1033f..31a57f9205db 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/nightly/ServiceIngestion.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/nightly/ServiceIngestion.spec.ts @@ -471,12 +471,13 @@ test.describe.serial( }) => { test.slow(); - // Mock the pipelineStatus endpoint to simulate high latency await page.route( `**/api/v1/services/ingestionPipelines/${encodeURIComponent( slowTestPipeline.fullyQualifiedName )}/pipelineStatus**`, async (route) => { + // Mock the pipelineStatus endpoint to simulate high latency + // eslint-disable-next-line playwright/no-wait-for-timeout await page.waitForTimeout(8000); await route.continue(); } @@ -504,27 +505,23 @@ test.describe.serial( `[data-row-key*="${slowTestPipeline.name}"]` ); - await expect(pipelineRow).toBeVisible({ timeout: 15000 }); + await expect(pipelineRow).toBeVisible(); + + // skeleton while the slow pipelineStatus API is still in-flight — + // confirming the UI reflects the pending state in both columns + await expect(pipelineRow.locator('.ant-skeleton-input')).toHaveCount(2); // Action buttons must be visible immediately — before the slow pipelineStatus // API resolves — verifying permissions don't wait on run history - await expect(pipelineRow.getByTestId('pause-button')).toBeVisible({ - timeout: 5000, - }); + await expect(pipelineRow.getByTestId('pause-button')).toBeVisible(); - await expect(pipelineRow.getByTestId('logs-button')).toBeVisible({ - timeout: 5000, - }); + await expect(pipelineRow.getByTestId('logs-button')).toBeVisible(); - await expect(pipelineRow.getByTestId('more-actions')).toBeVisible({ - timeout: 5000, - }); + await expect(pipelineRow.getByTestId('more-actions')).toBeVisible(); // Open the more-actions dropdown and verify the run button is present await pipelineRow.getByTestId('more-actions').click(); - await expect(page.getByTestId('run-button')).toBeVisible({ - timeout: 5000, - }); + await expect(page.getByTestId('run-button')).toBeVisible(); // Trigger a pipeline run via the run button const triggerResponse = page.waitForResponse( @@ -538,7 +535,7 @@ test.describe.serial( // Verify the run was triggered by checking the pipeline row shows a running state await expect( pipelineRow.getByTestId('pipeline-status').first() - ).toBeVisible({ timeout: 15000 }); + ).toBeVisible(); }); } ); From e0725c0a4bc8fd392d3fccf60b79d88e24b48e1f Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Fri, 10 Apr 2026 23:09:13 +0530 Subject: [PATCH 5/6] addressed gitar comment --- .../Ingestion/IngestionListTable/IngestionListTable.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 a43183854d4d..3aef37ca57bd 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 @@ -211,8 +211,7 @@ function IngestionListTable({ }; }, {}); setIngestionPipelinePermissions(permissionData); - }) - .catch((error) => showErrorToast(error as AxiosError)); + }); Promise.allSettled(recentRunStatusPromises) .then((recentRunStatusResponse) => { @@ -238,7 +237,6 @@ function IngestionListTable({ ); setRecentRunStatuses(recentRunStatusData); }) - .catch((error) => showErrorToast(error as AxiosError)) .finally(() => setIsIngestionRunsLoading(false)); }, [ingestionData]); From fac65c7fe6f180df4dfd048a50ac1051f8962fd7 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Fri, 10 Apr 2026 23:25:46 +0530 Subject: [PATCH 6/6] fixed lint checks --- .../IngestionListTable/IngestionListTable.tsx | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) 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 3aef37ca57bd..3c059d5746f1 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 @@ -201,17 +201,16 @@ function IngestionListTable({ ); // Fire both batches concurrently — whichever settles first updates state immediately - Promise.allSettled(permissionPromises) - .then((permissionResponse) => { - const permissionData = permissionResponse.reduce((acc, cv, index) => { - return { - ...acc, - [ingestionData?.[index].name]: - cv.status === 'fulfilled' ? cv.value : {}, - }; - }, {}); - setIngestionPipelinePermissions(permissionData); - }); + Promise.allSettled(permissionPromises).then((permissionResponse) => { + const permissionData = permissionResponse.reduce((acc, cv, index) => { + return { + ...acc, + [ingestionData?.[index].name]: + cv.status === 'fulfilled' ? cv.value : {}, + }; + }, {}); + setIngestionPipelinePermissions(permissionData); + }); Promise.allSettled(recentRunStatusPromises) .then((recentRunStatusResponse) => {