Skip to content

Commit 66df9ae

Browse files
Rohit0301SaaiAravindhRaja
authored andcommitted
fix: action buttons visible immediately despite slow pipelineStatus API (open-metadata#27139)
* fix: action buttons visible immediately despite slow pipelineStatus API * fixed the recent run overlapping issue * addressed gitar comment * addressed PR comment * addressed gitar comment * fixed lint checks
1 parent b5be103 commit 66df9ae

File tree

2 files changed

+194
-44
lines changed

2 files changed

+194
-44
lines changed

openmetadata-ui/src/main/resources/ui/playwright/e2e/nightly/ServiceIngestion.spec.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,156 @@ test.describe.serial(
386386
});
387387
}
388388
);
389+
390+
const slowPipelineService = new MysqlIngestionClass({
391+
shouldTestConnection: false,
392+
shouldAddIngestion: false,
393+
});
394+
let slowTestPipeline: {
395+
id: string;
396+
name: string;
397+
fullyQualifiedName: string;
398+
};
399+
400+
test.describe.serial(
401+
'Action buttons visible despite slow pipelineStatus API',
402+
PLAYWRIGHT_INGESTION_TAG_OBJ,
403+
() => {
404+
test.beforeEach('Navigate to database services', async ({ page }) => {
405+
await redirectToHomePage(page);
406+
await settingClick(
407+
page,
408+
slowPipelineService.category as unknown as SettingOptionsType
409+
);
410+
});
411+
412+
test('Setup: create MySQL service and ingestion pipeline', async ({
413+
page,
414+
}) => {
415+
await slowPipelineService.createService(page);
416+
417+
const { apiContext } = await getApiContext(page);
418+
419+
const serviceResponse = await apiContext
420+
.get(
421+
`/api/v1/services/databaseServices/name/${encodeURIComponent(
422+
slowPipelineService.getServiceName()
423+
)}`
424+
)
425+
.then((res) => res.json());
426+
427+
const createPipelineResponse = await apiContext.post(
428+
'/api/v1/services/ingestionPipelines',
429+
{
430+
data: {
431+
airflowConfig: {},
432+
loggerLevel: 'INFO',
433+
name: `${slowPipelineService.getServiceName()}-metadata`,
434+
pipelineType: 'metadata',
435+
service: {
436+
id: serviceResponse.id,
437+
type: 'databaseService',
438+
},
439+
sourceConfig: {
440+
config: {
441+
type: 'DatabaseMetadata',
442+
},
443+
},
444+
},
445+
}
446+
);
447+
448+
expect(createPipelineResponse.status()).toBe(201);
449+
const createdPipeline = await createPipelineResponse.json();
450+
451+
await apiContext.post(
452+
`/api/v1/services/ingestionPipelines/deploy/${createdPipeline.id}`
453+
);
454+
455+
slowTestPipeline = {
456+
id: createdPipeline.id,
457+
name: createdPipeline.name,
458+
fullyQualifiedName: createdPipeline.fullyQualifiedName,
459+
};
460+
});
461+
462+
/**
463+
* Validates that action buttons (logs, pause, run) are visible and functional
464+
* even when the pipelineStatus API response is delayed (simulated via route mock).
465+
*
466+
* Regression test for the issue where high pipelineStatus API latency blocked
467+
* rendering of action icons and the pause/resume button until the slow API resolved.
468+
*/
469+
test('Action buttons and pause visible when pipelineStatus API is slow', async ({
470+
page,
471+
}) => {
472+
test.slow();
473+
474+
await page.route(
475+
`**/api/v1/services/ingestionPipelines/${encodeURIComponent(
476+
slowTestPipeline.fullyQualifiedName
477+
)}/pipelineStatus**`,
478+
async (route) => {
479+
// Mock the pipelineStatus endpoint to simulate high latency
480+
// eslint-disable-next-line playwright/no-wait-for-timeout
481+
await page.waitForTimeout(8000);
482+
await route.continue();
483+
}
484+
);
485+
486+
await visitServiceDetailsPage(
487+
page,
488+
{
489+
type: slowPipelineService.category,
490+
name: slowPipelineService.getServiceName(),
491+
},
492+
false,
493+
false
494+
);
495+
496+
await page.getByTestId('data-assets-header').waitFor();
497+
await page.getByTestId('agents').click();
498+
499+
const metadataTab = page.locator('[data-testid="metadata-sub-tab"]');
500+
if (await metadataTab.isVisible()) {
501+
await metadataTab.click();
502+
}
503+
504+
const pipelineRow = page.locator(
505+
`[data-row-key*="${slowTestPipeline.name}"]`
506+
);
507+
508+
await expect(pipelineRow).toBeVisible();
509+
510+
// skeleton while the slow pipelineStatus API is still in-flight —
511+
// confirming the UI reflects the pending state in both columns
512+
await expect(pipelineRow.locator('.ant-skeleton-input')).toHaveCount(2);
513+
514+
// Action buttons must be visible immediately — before the slow pipelineStatus
515+
// API resolves — verifying permissions don't wait on run history
516+
await expect(pipelineRow.getByTestId('pause-button')).toBeVisible();
517+
518+
await expect(pipelineRow.getByTestId('logs-button')).toBeVisible();
519+
520+
await expect(pipelineRow.getByTestId('more-actions')).toBeVisible();
521+
522+
// Open the more-actions dropdown and verify the run button is present
523+
await pipelineRow.getByTestId('more-actions').click();
524+
await expect(page.getByTestId('run-button')).toBeVisible();
525+
526+
// Trigger a pipeline run via the run button
527+
const triggerResponse = page.waitForResponse(
528+
(res) =>
529+
res.url().includes('/services/ingestionPipelines/trigger/') &&
530+
res.request().method() === 'POST'
531+
);
532+
await page.getByTestId('run-button').click();
533+
await triggerResponse;
534+
535+
// Verify the run was triggered by checking the pipeline row shows a running state
536+
await expect(
537+
pipelineRow.getByTestId('pipeline-status').first()
538+
).toBeVisible();
539+
});
540+
}
541+
);

openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/IngestionListTable/IngestionListTable.tsx

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -186,60 +186,57 @@ function IngestionListTable({
186186
[handleCancelConfirmationModal]
187187
);
188188

189-
const fetchIngestionPipelineExtraDetails = useCallback(async () => {
190-
try {
191-
setIsIngestionRunsLoading(true);
192-
const permissionPromises = ingestionData.map((item) =>
193-
getEntityPermissionByFqn(
194-
ResourceEntity.INGESTION_PIPELINE,
195-
item.fullyQualifiedName ?? ''
196-
)
197-
);
198-
const recentRunStatusPromises = ingestionData.map((item) =>
199-
getRunHistoryForPipeline(item.fullyQualifiedName ?? '', { limit: 5 })
200-
);
201-
const permissionResponse = await Promise.allSettled(permissionPromises);
202-
const recentRunStatusResponse = await Promise.allSettled(
203-
recentRunStatusPromises
204-
);
189+
const fetchIngestionPipelineExtraDetails = useCallback(() => {
190+
setIsIngestionRunsLoading(true);
191+
192+
const permissionPromises = ingestionData.map((item) =>
193+
getEntityPermissionByFqn(
194+
ResourceEntity.INGESTION_PIPELINE,
195+
item.fullyQualifiedName ?? ''
196+
)
197+
);
205198

199+
const recentRunStatusPromises = ingestionData.map((item) =>
200+
getRunHistoryForPipeline(item.fullyQualifiedName ?? '', { limit: 5 })
201+
);
202+
203+
// Fire both batches concurrently — whichever settles first updates state immediately
204+
Promise.allSettled(permissionPromises).then((permissionResponse) => {
206205
const permissionData = permissionResponse.reduce((acc, cv, index) => {
207206
return {
208207
...acc,
209208
[ingestionData?.[index].name]:
210209
cv.status === 'fulfilled' ? cv.value : {},
211210
};
212211
}, {});
212+
setIngestionPipelinePermissions(permissionData);
213+
});
213214

214-
const recentRunStatusData = recentRunStatusResponse.reduce(
215-
(acc, cv, index) => {
216-
let value: PipelineStatus[] = [];
217-
218-
if (cv.status === 'fulfilled') {
219-
const runs = cv.value.data ?? [];
220-
221-
const ingestion = ingestionData[index];
215+
Promise.allSettled(recentRunStatusPromises)
216+
.then((recentRunStatusResponse) => {
217+
const recentRunStatusData = recentRunStatusResponse.reduce(
218+
(acc, cv, index) => {
219+
let value: PipelineStatus[] = [];
222220

223-
value =
224-
runs.length === 0 && ingestion?.pipelineStatuses
225-
? [ingestion.pipelineStatuses]
226-
: runs;
227-
}
221+
if (cv.status === 'fulfilled') {
222+
const runs = cv.value.data ?? [];
223+
const ingestion = ingestionData[index];
224+
value =
225+
runs.length === 0 && ingestion?.pipelineStatuses
226+
? [ingestion.pipelineStatuses]
227+
: runs;
228+
}
228229

229-
return {
230-
...acc,
231-
[ingestionData?.[index].name]: value,
232-
};
233-
},
234-
{}
235-
);
236-
setIngestionPipelinePermissions(permissionData);
237-
setRecentRunStatuses(recentRunStatusData);
238-
} catch (error) {
239-
showErrorToast(error as AxiosError);
240-
} finally {
241-
setIsIngestionRunsLoading(false);
242-
}
230+
return {
231+
...acc,
232+
[ingestionData?.[index].name]: value,
233+
};
234+
},
235+
{}
236+
);
237+
setRecentRunStatuses(recentRunStatusData);
238+
})
239+
.finally(() => setIsIngestionRunsLoading(false));
243240
}, [ingestionData]);
244241

245242
const { isFetchingStatus, platform } = useMemo(
@@ -379,7 +376,7 @@ function IngestionListTable({
379376
title: t('label.recent-run-plural'),
380377
dataIndex: 'recentRuns',
381378
key: 'recentRuns',
382-
width: 150,
379+
width: 180,
383380
render: (_: string, record: IngestionPipeline) => (
384381
<IngestionRecentRuns
385382
appRuns={recentRunStatuses[record.name]}

0 commit comments

Comments
 (0)