Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,156 @@ 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();

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we are adding manual wait?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShaileshParmar11 This timeout is added to increase the latency of that API to test few changes on UI.

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();

// 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();

await expect(pipelineRow.getByTestId('logs-button')).toBeVisible();

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();

// 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();
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -186,60 +186,57 @@ 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,
[ingestionData?.[index].name]:
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);
})
.finally(() => setIsIngestionRunsLoading(false));
}, [ingestionData]);

const { isFetchingStatus, platform } = useMemo(
Expand Down Expand Up @@ -379,7 +376,7 @@ function IngestionListTable({
title: t('label.recent-run-plural'),
dataIndex: 'recentRuns',
key: 'recentRuns',
width: 150,
width: 180,
render: (_: string, record: IngestionPipeline) => (
<IngestionRecentRuns
appRuns={recentRunStatuses[record.name]}
Expand Down
Loading