diff --git a/src/containers/Operations/Operations.tsx b/src/containers/Operations/Operations.tsx
index 3285ca681..5749621bf 100644
--- a/src/containers/Operations/Operations.tsx
+++ b/src/containers/Operations/Operations.tsx
@@ -1,12 +1,10 @@
import React from 'react';
-import {AccessDenied} from '../../components/Errors/403';
-import {ResponseError} from '../../components/Errors/ResponseError';
+import {PageError} from '../../components/Errors/PageError/PageError';
import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable';
import {TableSkeleton} from '../../components/TableSkeleton/TableSkeleton';
import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout';
import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants';
-import {isAccessError} from '../../utils/response';
import {OperationsControls} from './OperationsControls';
import {getColumns} from './columns';
@@ -41,8 +39,8 @@ export function Operations({database, scrollContainerRef}: OperationsProps) {
};
}, []);
- if (isAccessError(error)) {
- return ;
+ if (error) {
+ return ;
}
return (
@@ -58,7 +56,6 @@ export function Operations({database, scrollContainerRef}: OperationsProps) {
handleSearchChange={handleSearchChange}
/>
- {error ? : null}
{operations.length > 0 || isLoading ? (
({
getOperationList: build.infiniteQuery<
@@ -33,7 +47,9 @@ export const operationsApi = api.injectEndpoints({
page_token: pageParam,
};
const data = await window.api.operation.getOperationList(params, {signal});
- return {data};
+ // Validate and normalize the response
+ const validatedData = validateOperationListResponse(data);
+ return {data: validatedData};
} catch (error) {
return {error};
}
diff --git a/tests/suites/tenant/diagnostics/tabs/OperationsModel.ts b/tests/suites/tenant/diagnostics/tabs/OperationsModel.ts
index e1908fe7d..c6bbd1f62 100644
--- a/tests/suites/tenant/diagnostics/tabs/OperationsModel.ts
+++ b/tests/suites/tenant/diagnostics/tabs/OperationsModel.ts
@@ -15,6 +15,9 @@ export class OperationsTable extends BaseModel {
private scrollContainer: Locator;
private accessDeniedState: Locator;
private accessDeniedTitle: Locator;
+ private pageErrorState: Locator;
+ private pageErrorTitle: Locator;
+ private pageErrorDescription: Locator;
constructor(page: Page) {
super(page, page.locator('.kv-tenant-diagnostics'));
@@ -27,6 +30,10 @@ export class OperationsTable extends BaseModel {
// AccessDenied component is rendered at the root level of Operations component
this.accessDeniedState = page.locator('.kv-tenant-diagnostics .empty-state');
this.accessDeniedTitle = this.accessDeniedState.locator('.empty-state__title');
+ // PageError component also uses empty-state but with error illustration
+ this.pageErrorState = page.locator('.kv-tenant-diagnostics .empty-state');
+ this.pageErrorTitle = this.pageErrorState.locator('.empty-state__title');
+ this.pageErrorDescription = this.pageErrorState.locator('.empty-state__description .error');
}
async waitForTableVisible() {
@@ -194,4 +201,22 @@ export class OperationsTable extends BaseModel {
// Now get the actual new count
return await this.getOperationsCount();
}
+
+ async isPageErrorVisible(): Promise {
+ try {
+ await this.pageErrorState.waitFor({state: 'visible', timeout: VISIBILITY_TIMEOUT});
+ // Check if it has error description to distinguish from access denied
+ return await this.pageErrorDescription.isVisible();
+ } catch {
+ return false;
+ }
+ }
+
+ async getPageErrorTitle(): Promise {
+ return await this.pageErrorTitle.innerText();
+ }
+
+ async getPageErrorDescription(): Promise {
+ return await this.pageErrorDescription.innerText();
+ }
}
diff --git a/tests/suites/tenant/diagnostics/tabs/operations.test.ts b/tests/suites/tenant/diagnostics/tabs/operations.test.ts
index e719454ec..4928c2dac 100644
--- a/tests/suites/tenant/diagnostics/tabs/operations.test.ts
+++ b/tests/suites/tenant/diagnostics/tabs/operations.test.ts
@@ -6,8 +6,11 @@ import {Diagnostics, DiagnosticsTab} from '../Diagnostics';
import {
setupEmptyOperationsMock,
+ setupMalformedOperationsMock,
setupOperation403Mock,
+ setupOperationNetworkErrorMock,
setupOperationsMock,
+ setupPartialMalformedOperationsMock,
} from './operationsMocks';
test.describe('Operations Tab - Infinite Query', () => {
@@ -160,6 +163,129 @@ test.describe('Operations Tab - Infinite Query', () => {
expect(accessDeniedTitle).toBe('Access denied');
});
+ test('shows error state when operations request returns network error', async ({page}) => {
+ // Setup network error mock (simulates CORS blocking)
+ await setupOperationNetworkErrorMock(page);
+
+ const pageQueryParams = {
+ schema: tenantName,
+ database: tenantName,
+ tenantPage: 'diagnostics',
+ };
+
+ const tenantPageInstance = new TenantPage(page);
+ await tenantPageInstance.goto(pageQueryParams);
+
+ const diagnostics = new Diagnostics(page);
+ await diagnostics.clickTab(DiagnosticsTab.Operations);
+ // Wait a bit for potential loading
+ await page.waitForTimeout(2000);
+
+ // Wait for error state to be visible
+ const isPageErrorVisible = await diagnostics.operations.isPageErrorVisible();
+ expect(isPageErrorVisible).toBe(true);
+
+ // Verify the error title
+ const errorTitle = await diagnostics.operations.getPageErrorTitle();
+ expect(errorTitle).toBe('Error');
+
+ // Verify the error description shows network error
+ const errorDescription = await diagnostics.operations.getPageErrorDescription();
+ expect(errorDescription.toLowerCase()).toContain('network');
+ });
+
+ test('handles malformed response without operations array', async ({page}) => {
+ // Setup malformed response mock (returns status SUCCESS but no operations array)
+ await setupMalformedOperationsMock(page);
+
+ const pageQueryParams = {
+ schema: tenantName,
+ database: tenantName,
+ tenantPage: 'diagnostics',
+ };
+
+ const tenantPageInstance = new TenantPage(page);
+ await tenantPageInstance.goto(pageQueryParams);
+
+ const diagnostics = new Diagnostics(page);
+ await diagnostics.clickTab(DiagnosticsTab.Operations);
+
+ // Wait for table to be visible
+ await diagnostics.operations.waitForTableVisible();
+ await diagnostics.operations.waitForDataLoad();
+
+ // Verify empty state is shown
+ const isEmptyVisible = await diagnostics.operations.isEmptyStateVisible();
+ expect(isEmptyVisible).toBe(true);
+
+ // Verify no data rows
+ const rowCount = await diagnostics.operations.getRowCount();
+ expect(rowCount).toBeLessThanOrEqual(1);
+
+ // Verify operations count is 0
+ const operationsCount = await diagnostics.operations.getOperationsCount();
+ expect(operationsCount).toBe(0);
+
+ // Wait to ensure no infinite refetching occurs
+ await page.waitForTimeout(3000);
+
+ // Verify the count is still 0 (no infinite refetching)
+ const finalOperationsCount = await diagnostics.operations.getOperationsCount();
+ expect(finalOperationsCount).toBe(0);
+ });
+
+ test('stops pagination when receiving malformed response after valid data', async ({page}) => {
+ // Setup mock that returns valid data first, then malformed response
+ await setupPartialMalformedOperationsMock(page);
+
+ const pageQueryParams = {
+ schema: tenantName,
+ database: tenantName,
+ tenantPage: 'diagnostics',
+ };
+
+ const tenantPageInstance = new TenantPage(page);
+ await tenantPageInstance.goto(pageQueryParams);
+
+ const diagnostics = new Diagnostics(page);
+ await diagnostics.clickTab(DiagnosticsTab.Operations);
+
+ // Wait for initial data to load
+ await diagnostics.operations.waitForTableVisible();
+ await diagnostics.operations.waitForDataLoad();
+
+ // Verify initial page loaded (should have 20 operations)
+ const initialOperationsCount = await diagnostics.operations.getOperationsCount();
+ expect(initialOperationsCount).toBe(20);
+
+ // Verify first row data
+ const firstRowData = await diagnostics.operations.getRowData(0);
+ expect(firstRowData['Operation ID']).toBeTruthy();
+
+ // Scroll to bottom to trigger next page load
+ await diagnostics.operations.scrollToBottom();
+
+ // Wait a bit for potential loading
+ await page.waitForTimeout(2000);
+
+ // Check if loading more appears and disappears
+ const isLoadingVisible = await diagnostics.operations.isLoadingMoreVisible();
+ if (isLoadingVisible) {
+ await diagnostics.operations.waitForLoadingMoreToDisappear();
+ }
+
+ // Verify the count remains at 20 (malformed response didn't add more)
+ const finalOperationsCount = await diagnostics.operations.getOperationsCount();
+ expect(finalOperationsCount).toBe(20);
+
+ // Wait to ensure no infinite refetching occurs
+ await page.waitForTimeout(3000);
+
+ // Verify the count is still 20
+ const stillFinalCount = await diagnostics.operations.getOperationsCount();
+ expect(stillFinalCount).toBe(20);
+ });
+
test('loads all operations when scrolling to the bottom multiple times', async ({page}) => {
// Setup mocks with 80 operations (4 pages of 20)
await setupOperationsMock(page, {totalOperations: 80});
diff --git a/tests/suites/tenant/diagnostics/tabs/operationsMocks.ts b/tests/suites/tenant/diagnostics/tabs/operationsMocks.ts
index ee333dc07..0365f7474 100644
--- a/tests/suites/tenant/diagnostics/tabs/operationsMocks.ts
+++ b/tests/suites/tenant/diagnostics/tabs/operationsMocks.ts
@@ -245,3 +245,71 @@ export const setupAllOperationMocks = async (page: Page, options?: {totalOperati
await setupOperationsMock(page, options);
await setupOperationMutationMocks(page);
};
+
+export const setupOperationNetworkErrorMock = async (page: Page) => {
+ await page.route(`${backend}/operation/list*`, async (route) => {
+ // Simulate a network error by aborting the request
+ // This mimics what happens when CORS blocks a request
+ await route.abort('failed');
+ });
+};
+
+export const setupMalformedOperationsMock = async (page: Page) => {
+ await page.route(`${backend}/operation/list*`, async (route) => {
+ // Return a response with status SUCCESS but missing operations array
+ const response = {
+ status: 'SUCCESS',
+ next_page_token: '1',
+ // operations array is missing
+ };
+
+ await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
+
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify(response),
+ });
+ });
+};
+
+export const setupPartialMalformedOperationsMock = async (page: Page) => {
+ let requestCount = 0;
+
+ await page.route(`${backend}/operation/list*`, async (route) => {
+ requestCount++;
+
+ // First request returns valid data
+ if (requestCount === 1) {
+ const operations = generateBuildIndexOperations(0, 20);
+ const response = {
+ next_page_token: '1',
+ status: 'SUCCESS',
+ operations,
+ };
+
+ await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
+
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify(response),
+ });
+ } else {
+ // Subsequent requests return malformed response
+ const response = {
+ status: 'SUCCESS',
+ next_page_token: '2',
+ // operations array is missing
+ };
+
+ await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
+
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify(response),
+ });
+ }
+ });
+};