Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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,159 @@
});
}
);

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

Check warning on line 480 in openmetadata-ui/src/main/resources/ui/playwright/e2e/nightly/ServiceIngestion.spec.ts

View workflow job for this annotation

GitHub Actions / lint-playwright

Unexpected use of page.waitForTimeout()
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({ 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 });
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.

in every step i see random timeout, what the reason of it, and do we even need it?

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.

added this timeout is added because we need to check few things on UI after the api response came for the api for which we have increased the latency by mocking it.

});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -186,60 +186,60 @@ 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 permissionData = permissionResponse.reduce((acc, cv, index) => {
return {
...acc,
[ingestionData?.[index].name]:
cv.status === 'fulfilled' ? cv.value : {},
};
}, {});
const fetchIngestionPipelineExtraDetails = useCallback(() => {
setIsIngestionRunsLoading(true);

const recentRunStatusData = recentRunStatusResponse.reduce(
(acc, cv, index) => {
let value: PipelineStatus[] = [];
const permissionPromises = ingestionData.map((item) =>
getEntityPermissionByFqn(
ResourceEntity.INGESTION_PIPELINE,
item.fullyQualifiedName ?? ''
)
);

if (cv.status === 'fulfilled') {
const runs = cv.value.data ?? [];

const ingestion = ingestionData[index];

value =
runs.length === 0 && ingestion?.pipelineStatuses
? [ingestion.pipelineStatuses]
: runs;
}
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]: value,
[ingestionData?.[index].name]:
cv.status === 'fulfilled' ? cv.value : {},
};
},
{}
);
setIngestionPipelinePermissions(permissionData);
setRecentRunStatuses(recentRunStatusData);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsIngestionRunsLoading(false);
}
}, {});
setIngestionPipelinePermissions(permissionData);
})
.catch((error) => showErrorToast(error as AxiosError));
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Outdated

Promise.allSettled(recentRunStatusPromises)
.then((recentRunStatusResponse) => {
const recentRunStatusData = recentRunStatusResponse.reduce(
(acc, cv, index) => {
let value: PipelineStatus[] = [];

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,
};
},
{}
);
setRecentRunStatuses(recentRunStatusData);
})
.catch((error) => showErrorToast(error as AxiosError))
.finally(() => setIsIngestionRunsLoading(false));
}, [ingestionData]);

const { isFetchingStatus, platform } = useMemo(
Expand Down Expand Up @@ -379,7 +379,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