From e74d8b25934d38795b46ab59946abad6a1714718 Mon Sep 17 00:00:00 2001 From: Dhruv parmar Date: Thu, 20 Feb 2025 17:48:45 +0530 Subject: [PATCH 1/9] fixed: pagination for deleted users --- .../ui/playwright/e2e/Pages/Users.spec.ts | 81 +++++++++++++++++++ .../ui/playwright/support/user/UserClass.ts | 4 +- .../src/pages/UserListPage/UserListPageV1.tsx | 5 +- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts index fb74e92382d0..1a11cb26c098 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts @@ -447,3 +447,84 @@ test.describe('User with Data Steward Roles', () => { await visitOwnProfilePage(dataStewardPage); }); }); + +test.describe('Soft Delete User', () => { + test('Create and soft delete 30 users', async ({ browser, adminPage }) => { + const { apiContext } = await performAdminLogin(browser); + + const users = []; + try { + await redirectToHomePage(adminPage); + await visitUserListPage(adminPage); + + for (let i = 0; i < 3; i++) { + const testUser = new UserClass(); + await testUser.create(apiContext); + users.push(testUser); + } + + // Soft delete all created users + for (const testUser of users) { + await testUser.delete(apiContext, false); + } + + const userResponsePromise = adminPage.waitForResponse( + '/api/v1/users?*include=non-deleted' + ); + await redirectToHomePage(adminPage); + + await settingClick(adminPage, GlobalSettingOptions.USERS); + await userResponsePromise; + + const deletedUserResponsePromise = adminPage.waitForResponse( + (response) => { + const url = response.url(); + const method = response.request().method(); + + // Ensure the request starts with the expected base URL + if ( + !url.includes('/api/v1/users?') || + !url.includes('include=deleted') + ) { + return false; + } + + // Parse the URL parameters + const urlObj = new URL(url); + const params = urlObj.searchParams; + + // Ensure required query parameters exist + const hasValidParams = + params.has('isBot') && + params.has('fields') && + params.has('limit') && + params.has('isAdmin'); + + return hasValidParams && method === 'GET'; + } + ); + + await adminPage.click('[data-testid="show-deleted"]'); + + const response = await deletedUserResponsePromise; + const url = new URL(response.url()); + + expect(url.searchParams.get('limit')).toBe('25'); + + const nextButton = adminPage.locator('[data-testid="next"]'); + + await expect(nextButton).toBeVisible(); + + if (await nextButton.isVisible()) { + await nextButton.click(); + } else { + throw new Error('Next button is not visible. Test failed.'); + } + } finally { + // Cleanup: Permanently delete all created users + for (const testUser of users) { + await testUser.delete(apiContext); + } + } + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts index 04ae6924ca57..3f9b8497202b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts @@ -127,7 +127,7 @@ export class UserClass { await dataStewardTeam.create(apiContext); } - async delete(apiContext: APIRequestContext) { + async delete(apiContext: APIRequestContext, hardDelete = true) { if (this.isUserDataSteward) { await dataStewardPolicy.delete(apiContext); await dataStewardRoles.delete(apiContext); @@ -135,7 +135,7 @@ export class UserClass { } const response = await apiContext.delete( - `/api/v1/users/${this.responseData.id}?recursive=false&hardDelete=true` + `/api/v1/users/${this.responseData.id}?recursive=false&hardDelete=${hardDelete}` ); return response.body; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx index 08957e5818bc..c30bcd3fca73 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.tsx @@ -123,7 +123,6 @@ const UserListPageV1 = () => { limit: pageSize, ...params, }); - setUserList(data); handlePagingChange(userPaging); } catch (error) { @@ -223,12 +222,13 @@ const UserListPageV1 = () => { const handleShowDeletedUserChange = (value: boolean) => { handlePageChange(INITIAL_PAGING_VALUE); + handlePageSizeChange(PAGE_SIZE_MEDIUM); setSearchValue(''); setShowDeletedUser(value); fetchUsersList({ isAdmin: isAdminPage, include: value ? Include.Deleted : Include.NonDeleted, - limit: pageSize, + limit: PAGE_SIZE_MEDIUM, }); }; @@ -269,6 +269,7 @@ const UserListPageV1 = () => { } else { fetchUsersList({ isAdmin: isAdminPage, + include: showDeletedUser ? Include.Deleted : Include.NonDeleted, }); } } else { From e472034f96c7ab2fbbe133f4381429d81101726f Mon Sep 17 00:00:00 2001 From: Dhruv parmar Date: Fri, 21 Feb 2025 13:41:17 +0530 Subject: [PATCH 2/9] fixed test cases --- .../e2e/Flow/UsersPagination.spec.ts | 88 +++++++++++++++++++ .../ui/playwright/e2e/Pages/Users.spec.ts | 81 ----------------- .../UserListPage/UserListPageV1.test.tsx | 2 + 3 files changed, 90 insertions(+), 81 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts new file mode 100644 index 000000000000..3082c9f9ef94 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, Page, test as base } from '@playwright/test'; +import { GlobalSettingOptions } from '../../constant/settings'; +import { UserClass } from '../../support/user/UserClass'; +import { performAdminLogin } from '../../utils/admin'; +import { redirectToHomePage } from '../../utils/common'; +import { settingClick } from '../../utils/sidebar'; +import { visitUserListPage } from '../../utils/user'; + +const adminUser = new UserClass(); +const users: UserClass[] = []; + +const test = base.extend<{ + adminPage: Page; +}>({ + adminPage: async ({ browser }, use) => { + const adminPage = await browser.newPage(); + await adminUser.login(adminPage); + await use(adminPage); + await adminPage.close(); + }, +}); + +test.describe('Soft Delete User Pagination', () => { + test.beforeAll('Creating and Soft Deleting 30 users', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + + await adminUser.create(apiContext); + await adminUser.setAdminRole(apiContext); + // Create and soft delete users + for (let i = 0; i < 30; i++) { + const testUser = new UserClass(); + await testUser.create(apiContext); + await testUser.delete(apiContext, false); + users.push(testUser); + } + await afterAction(); + }); + + test.beforeEach('Redirecting to user list', async ({ adminPage }) => { + await redirectToHomePage(adminPage); + await visitUserListPage(adminPage); + + const userResponsePromise = adminPage.waitForResponse( + '/api/v1/users?*include=non-deleted' + ); + + await settingClick(adminPage, GlobalSettingOptions.USERS); + await userResponsePromise; + }); + + test.afterAll('Permanently deleting users', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + for (const testUser of users) { + await testUser.delete(apiContext); + } + await adminUser.delete(apiContext); + await afterAction(); + }); + + test('Testing user API calls and pagination', async ({ adminPage }) => { + const expectedUrl = + '**/api/v1/users?isBot=false&fields=profile%2Cteams%2Croles&limit=25&isAdmin=false&include=deleted'; + + const deletedUserResponsePromise = adminPage.waitForResponse(expectedUrl); + + await adminPage.click('[data-testid="show-deleted"]'); + + const response = await deletedUserResponsePromise; + + expect(response.ok()).toBeTruthy(); + + const nextButton = adminPage.locator('[data-testid="next"]'); + + await nextButton.click(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts index 1a11cb26c098..fb74e92382d0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts @@ -447,84 +447,3 @@ test.describe('User with Data Steward Roles', () => { await visitOwnProfilePage(dataStewardPage); }); }); - -test.describe('Soft Delete User', () => { - test('Create and soft delete 30 users', async ({ browser, adminPage }) => { - const { apiContext } = await performAdminLogin(browser); - - const users = []; - try { - await redirectToHomePage(adminPage); - await visitUserListPage(adminPage); - - for (let i = 0; i < 3; i++) { - const testUser = new UserClass(); - await testUser.create(apiContext); - users.push(testUser); - } - - // Soft delete all created users - for (const testUser of users) { - await testUser.delete(apiContext, false); - } - - const userResponsePromise = adminPage.waitForResponse( - '/api/v1/users?*include=non-deleted' - ); - await redirectToHomePage(adminPage); - - await settingClick(adminPage, GlobalSettingOptions.USERS); - await userResponsePromise; - - const deletedUserResponsePromise = adminPage.waitForResponse( - (response) => { - const url = response.url(); - const method = response.request().method(); - - // Ensure the request starts with the expected base URL - if ( - !url.includes('/api/v1/users?') || - !url.includes('include=deleted') - ) { - return false; - } - - // Parse the URL parameters - const urlObj = new URL(url); - const params = urlObj.searchParams; - - // Ensure required query parameters exist - const hasValidParams = - params.has('isBot') && - params.has('fields') && - params.has('limit') && - params.has('isAdmin'); - - return hasValidParams && method === 'GET'; - } - ); - - await adminPage.click('[data-testid="show-deleted"]'); - - const response = await deletedUserResponsePromise; - const url = new URL(response.url()); - - expect(url.searchParams.get('limit')).toBe('25'); - - const nextButton = adminPage.locator('[data-testid="next"]'); - - await expect(nextButton).toBeVisible(); - - if (await nextButton.isVisible()) { - await nextButton.click(); - } else { - throw new Error('Next button is not visible. Test failed.'); - } - } finally { - // Cleanup: Permanently delete all created users - for (const testUser of users) { - await testUser.delete(apiContext); - } - } - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.test.tsx index d2868879595d..8590c2ebc683 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserListPage/UserListPageV1.test.tsx @@ -114,6 +114,7 @@ describe('Test UserListPage component', () => { expect(getUsers).toHaveBeenCalledWith({ fields: 'profile,teams,roles', + include: 'non-deleted', isAdmin: false, isBot: false, limit: 25, @@ -141,6 +142,7 @@ describe('Test UserListPage component', () => { expect(getUsers).toHaveBeenCalledWith({ fields: 'profile,teams,roles', + include: 'non-deleted', isAdmin: false, isBot: false, limit: 25, From bf9283b96d873fae0f2295608f0bb643faf0c125 Mon Sep 17 00:00:00 2001 From: Dhruv parmar Date: Fri, 21 Feb 2025 14:05:54 +0530 Subject: [PATCH 3/9] fixed comments --- .../ui/playwright/e2e/Flow/UsersPagination.spec.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts index 3082c9f9ef94..d19f771268a2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts @@ -16,7 +16,6 @@ import { UserClass } from '../../support/user/UserClass'; import { performAdminLogin } from '../../utils/admin'; import { redirectToHomePage } from '../../utils/common'; import { settingClick } from '../../utils/sidebar'; -import { visitUserListPage } from '../../utils/user'; const adminUser = new UserClass(); const users: UserClass[] = []; @@ -50,8 +49,6 @@ test.describe('Soft Delete User Pagination', () => { test.beforeEach('Redirecting to user list', async ({ adminPage }) => { await redirectToHomePage(adminPage); - await visitUserListPage(adminPage); - const userResponsePromise = adminPage.waitForResponse( '/api/v1/users?*include=non-deleted' ); @@ -82,7 +79,17 @@ test.describe('Soft Delete User Pagination', () => { expect(response.ok()).toBeTruthy(); const nextButton = adminPage.locator('[data-testid="next"]'); + const expectedUrlPattern = + /\/api\/v1\/users\?isBot=false&fields=profile%2Cteams%2Croles&limit=25&isAdmin=false&after=.*?&include=deleted/; + + const paginatedResponsePromise = adminPage.waitForResponse((response) => { + const url = response.url(); + return expectedUrlPattern.test(url); + }); await nextButton.click(); + const paginatedResponse = await paginatedResponsePromise; + + expect(paginatedResponse.ok()).toBeTruthy(); }); }); From 130317280f8605e93eaf19d17c6e8c3b5b0b83c1 Mon Sep 17 00:00:00 2001 From: Dhruv parmar Date: Mon, 24 Feb 2025 11:01:15 +0530 Subject: [PATCH 4/9] used admin token to authenticate --- .../e2e/Flow/UsersPagination.spec.ts | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts index d19f771268a2..c6f0548f1597 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/UsersPagination.spec.ts @@ -10,30 +10,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { expect, Page, test as base } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { GlobalSettingOptions } from '../../constant/settings'; import { UserClass } from '../../support/user/UserClass'; -import { performAdminLogin } from '../../utils/admin'; -import { redirectToHomePage } from '../../utils/common'; +import { createNewPage, redirectToHomePage } from '../../utils/common'; import { settingClick } from '../../utils/sidebar'; const adminUser = new UserClass(); const users: UserClass[] = []; -const test = base.extend<{ - adminPage: Page; -}>({ - adminPage: async ({ browser }, use) => { - const adminPage = await browser.newPage(); - await adminUser.login(adminPage); - await use(adminPage); - await adminPage.close(); - }, -}); +test.use({ storageState: 'playwright/.auth/admin.json' }); test.describe('Soft Delete User Pagination', () => { test.beforeAll('Creating and Soft Deleting 30 users', async ({ browser }) => { - const { apiContext, afterAction } = await performAdminLogin(browser); + const { apiContext, afterAction } = await createNewPage(browser); await adminUser.create(apiContext); await adminUser.setAdminRole(apiContext); @@ -47,18 +37,18 @@ test.describe('Soft Delete User Pagination', () => { await afterAction(); }); - test.beforeEach('Redirecting to user list', async ({ adminPage }) => { - await redirectToHomePage(adminPage); - const userResponsePromise = adminPage.waitForResponse( + test.beforeEach('Redirecting to user list', async ({ page }) => { + await redirectToHomePage(page); + const userResponsePromise = page.waitForResponse( '/api/v1/users?*include=non-deleted' ); - await settingClick(adminPage, GlobalSettingOptions.USERS); + await settingClick(page, GlobalSettingOptions.USERS); await userResponsePromise; }); test.afterAll('Permanently deleting users', async ({ browser }) => { - const { apiContext, afterAction } = await performAdminLogin(browser); + const { apiContext, afterAction } = await createNewPage(browser); for (const testUser of users) { await testUser.delete(apiContext); } @@ -66,23 +56,23 @@ test.describe('Soft Delete User Pagination', () => { await afterAction(); }); - test('Testing user API calls and pagination', async ({ adminPage }) => { + test('Testing user API calls and pagination', async ({ page }) => { const expectedUrl = '**/api/v1/users?isBot=false&fields=profile%2Cteams%2Croles&limit=25&isAdmin=false&include=deleted'; - const deletedUserResponsePromise = adminPage.waitForResponse(expectedUrl); + const deletedUserResponsePromise = page.waitForResponse(expectedUrl); - await adminPage.click('[data-testid="show-deleted"]'); + await page.click('[data-testid="show-deleted"]'); const response = await deletedUserResponsePromise; expect(response.ok()).toBeTruthy(); - const nextButton = adminPage.locator('[data-testid="next"]'); + const nextButton = page.locator('[data-testid="next"]'); const expectedUrlPattern = /\/api\/v1\/users\?isBot=false&fields=profile%2Cteams%2Croles&limit=25&isAdmin=false&after=.*?&include=deleted/; - const paginatedResponsePromise = adminPage.waitForResponse((response) => { + const paginatedResponsePromise = page.waitForResponse((response) => { const url = response.url(); return expectedUrlPattern.test(url); From 9d74ecb21588d13c4be7d07d4b381f3834dd02d0 Mon Sep 17 00:00:00 2001 From: Dhruv parmar Date: Wed, 26 Feb 2025 10:01:06 +0530 Subject: [PATCH 5/9] feature: added Diagnostic info tab --- .../e2e/Flow/ObservabilityAlerts.spec.ts | 34 ++++++ .../AlertDiagnosticInfoTab.interface.ts | 27 ++++ .../AlertDiagnosticInfoTab.test.tsx | 68 +++++++++++ .../AlertDiagnosticInfoTab.tsx | 115 ++++++++++++++++++ .../alert-diagnostic-info-tab.less | 68 +++++++++++ .../resources/ui/src/enums/Alerts.enum.ts | 1 + .../ui/src/locale/languages/en-us.json | 25 +++- .../resources/ui/src/mocks/Alerts.mock.ts | 22 ++++ .../AlertDetailsPage/AlertDetailsPage.tsx | 50 +++++++- .../resources/ui/src/rest/observabilityAPI.ts | 14 +++ 10 files changed, 416 insertions(+), 8 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.interface.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/alert-diagnostic-info-tab.less diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ObservabilityAlerts.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ObservabilityAlerts.spec.ts index 8bd4d01a7eeb..b89e7e11e87d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ObservabilityAlerts.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ObservabilityAlerts.spec.ts @@ -381,3 +381,37 @@ test('Alert operations for a user with and without permissions', async ({ await deleteAlert(userWithPermissionsPage, data.alertDetails, false); }); }); + +test('Alert Diagnostic Info', async ({ page }) => { + const ALERT_NAME = generateAlertName(); + + await test.step('Create alert', async () => { + await visitObservabilityAlertPage(page); + data.alertDetails = await createAlert({ + page, + alertName: ALERT_NAME, + sourceName: SOURCE_NAME_1, + sourceDisplayName: SOURCE_DISPLAY_NAME_1, + user: user1, + createButtonId: 'create-observability', + selectId: 'Owner Name', + addTrigger: true, + }); + }); + + await test.step('Verify diagnostic info tab', async () => { + await visitObservabilityAlertPage(page); + await visitAlertDetailsPage(page, data.alertDetails); + + const diagnosticTab = page.getByRole('tab', { name: /diagnostic info/i }); + const diagnosticInfoResponse = page.waitForResponse( + `/api/v1/events/subscriptions/**/diagnosticInfo` + ); + await diagnosticTab.click(); + await diagnosticInfoResponse; + }); + + await test.step('Delete alert', async () => { + await deleteAlert(page, data.alertDetails, false); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.interface.ts new file mode 100644 index 000000000000..03058b71d9e5 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.interface.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface AlertDiagnosticData { + latestOffset: number; + currentOffset: number; + startingOffset: number; + hasProcessedAllEvents: boolean; + successfulEventsCount: number; + failedEventsCount: number; + relevantUnprocessedEventsCount: number; + totalUnprocessedEventsCount: number; +} + +export interface AlertDiagnosticInfoTabProps { + diagnosticData: AlertDiagnosticData; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.test.tsx new file mode 100644 index 000000000000..e4b448292cc2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { + mockDiagnosticData, + mockEmptyDiagnosticData, +} from '../../../../mocks/Alerts.mock'; +import AlertDiagnosticInfoTab from './AlertDiagnosticInfoTab'; + +describe('AlertDiagnosticInfoTab', () => { + it('should render all offset information correctly', () => { + render(); + + const inputs = screen.getAllByTestId('input-field'); + + // Check labels + expect(screen.getByText('label.latest-offset:')).toBeInTheDocument(); + expect(screen.getByText('label.current-offset:')).toBeInTheDocument(); + expect(screen.getByText('label.starting-offset:')).toBeInTheDocument(); + expect( + screen.getByText('label.successful-event-plural:') + ).toBeInTheDocument(); + expect(screen.getByText('label.failed-event-plural:')).toBeInTheDocument(); + expect( + screen.getByText('label.processed-all-event-plural:') + ).toBeInTheDocument(); + + // Check input values + expect(inputs[0]).toHaveValue('100'); + expect(inputs[1]).toHaveValue('80'); + expect(inputs[2]).toHaveValue('0'); + expect(inputs[3]).toHaveValue('75'); + expect(inputs[4]).toHaveValue('5'); + expect(inputs[5]).toHaveValue('Yes'); + }); + + it('should render processed all events status as "No" when false', () => { + render(); + + const inputs = screen.getAllByTestId('input-field'); + + expect(inputs[5]).toHaveValue('No'); + }); + + it('should render with empty/zero values correctly', () => { + render(); + + const inputs = screen.getAllByTestId('input-field'); + + // Check numeric fields have value "0" + expect(inputs[0]).toHaveValue('0'); // latestOffset + expect(inputs[1]).toHaveValue('0'); // currentOffset + expect(inputs[2]).toHaveValue('0'); // startingOffset + expect(inputs[3]).toHaveValue('0'); // successfulEventsCount + expect(inputs[4]).toHaveValue('0'); // failedEventsCount + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.tsx new file mode 100644 index 000000000000..b0fbabcbecd2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.tsx @@ -0,0 +1,115 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Card, Col, Input, Row, Tooltip } from 'antd'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { GRAYED_OUT_COLOR } from '../../../../constants/constants'; +import './alert-diagnostic-info-tab.less'; +import { AlertDiagnosticInfoTabProps } from './AlertDiagnosticInfoTab.interface'; + +interface DiagnosticItem { + key: string; + value: string | number | boolean; + description: string; +} + +const DiagnosticItemRow = ({ item }: { item: DiagnosticItem }) => { + const { t } = useTranslation(); + + return ( + + + + +

{t(item.key)}:

+ + + +
+ + + + +
+ + ); +}; + +function AlertDiagnosticInfoTab({ + diagnosticData, +}: AlertDiagnosticInfoTabProps) { + const diagnosticItems = useMemo( + () => [ + { + key: 'label.latest-offset', + value: diagnosticData?.latestOffset, + description: 'The latest offset of the event in the system.', + }, + { + key: 'label.current-offset', + value: diagnosticData?.currentOffset, + description: 'The current offset of the event subscription.', + }, + { + key: 'label.starting-offset', + value: diagnosticData?.startingOffset, + description: + 'The initial offset of the event subscription when it started processing.', + }, + { + key: 'label.successful-event-plural', + value: diagnosticData?.successfulEventsCount, + description: 'Count of successful events for specific alert.', + }, + { + key: 'label.failed-event-plural', + value: diagnosticData?.failedEventsCount, + description: 'Count of failed events for specific alert.', + }, + { + key: 'label.processed-all-event-plural', + value: diagnosticData?.hasProcessedAllEvents, + description: 'Indicates whether all events have been processed.', + }, + ], + [diagnosticData] + ); + + return ( + + + {diagnosticItems.map((item) => ( + + ))} + + + ); +} + +export default React.memo(AlertDiagnosticInfoTab); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/alert-diagnostic-info-tab.less b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/alert-diagnostic-info-tab.less new file mode 100644 index 000000000000..b990482e63d2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Alerts/AlertDetails/AlertDiagnosticInfo/alert-diagnostic-info-tab.less @@ -0,0 +1,68 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.alert-diagnostic-container { + .ant-card { + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + + &:hover { + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + } + + .ant-input { + &[readonly] { + background-color: transparent; + cursor: default; + } + } + } + + .ant-statistic { + .ant-statistic-title { + color: rgba(0, 0, 0, 0.45); + font-size: 14px; + } + + .ant-statistic-content { + font-size: 24px; + font-weight: 600; + } + } + + .ant-typography { + margin-bottom: 16px; + } + + .text-grey-muted { + color: rgba(0, 0, 0, 0.45); + } +} + +.label-info-wrapper { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: 2px !important; + + .info-icon { + color: #c4c4c4; + font-size: 14px; + display: flex; + align-items: center; + } + + .text-grey-muted { + display: inline; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/Alerts.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/Alerts.enum.ts index 1513d8be8f3c..0a74e51d59ff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/Alerts.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/Alerts.enum.ts @@ -14,6 +14,7 @@ export enum AlertDetailTabs { CONFIGURATION = 'configuration', RECENT_EVENTS = 'recentEvents', + DIAGNOSTIC_INFO = 'diagnostic info', } export enum AlertRecentEventFilters { diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index b3544b9c0440..899bd8fe0f4e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -60,6 +60,7 @@ "alert-lowercase": "alert", "alert-lowercase-plural": "alerts", "alert-plural": "Alerts", + "alert-synced-successfully": "Alert synced successfully", "alert-type": "Alert Type", "algorithm": "Algorithm", "all": "All", @@ -249,6 +250,7 @@ "credentials-type": "Credentials Type", "criteria": "Criteria", "cron": "Cron", + "current-offset": "Current Offset", "current-version": "Current Version", "custom": "Custom", "custom-attribute-plural": "Custom Attributes", @@ -375,6 +377,7 @@ "destination-plural": "Destinations", "detail-plural": "Details", "developed-by-developer": "Developed by {{developer}}", + "diagnostic-info": "Diagnostic Info", "dimension": "Dimension", "disable": "Disable", "disable-lowercase": "disable", @@ -476,6 +479,7 @@ "error-plural": "Errors", "event-plural": "Events", "event-publisher-plural": "Event Publishers", + "event-statistics": "Event Statistics", "event-type": "Event Type", "event-type-lowercase": "event type", "every": "Every", @@ -501,6 +505,7 @@ "external": "External", "failed": "Failed", "failed-entity": "Failed {{entity}}", + "failed-event-plural": "Failed Events", "failing-subscription-id": "Failing Subscription Id", "failure-comment": "Failure Comment", "failure-context": "Failure Context", @@ -688,6 +693,7 @@ "last-run-result": "Last Run Result", "last-updated": "Last Updated", "latest": "Latest", + "latest-offset": "Latest Offset", "layer": "Layer", "layer-plural": "Layers", "learn-more": "Learn More", @@ -842,6 +848,7 @@ "observability-alert": "Observability Alert", "october": "October", "of-lowercase": "of", + "offset-information": "Offset Information", "ok": "Ok", "okta": "Okta", "okta-service-account-email": "Okta Service Account Email", @@ -944,6 +951,7 @@ "privacy-policy": "Privacy Policy", "private-key": "PrivateKey", "private-key-id": "Private Key ID", + "processed-all-event-plural": "Processed All Events", "profile": "Profile", "profile-config": "Profile config", "profile-lowercase": "profile", @@ -1004,6 +1012,7 @@ "relationship": "Relationship", "relationship-type": "Relationship Type", "relevance": "Relevance", + "relevant-unprocessed-event-plural": "Relevant Unprocessed Events", "remove": "Remove", "remove-entity": "Remove {{entity}}", "remove-entity-lowercase": "remove {{entity}}", @@ -1185,6 +1194,7 @@ "start-entity": "Start {{entity}}", "started": "Started", "started-following": "Started following", + "starting-offset": "Starting Offset", "status": "Status", "stay-up-to-date": "Stay Up-to-date", "step": "Step", @@ -1205,6 +1215,7 @@ "subscription": "Subscription", "success": "Success", "successful": "Successful", + "successful-event-plural": "Successful Events", "successfully-lowercase": "successfully", "successfully-uploaded": "Successfully Uploaded", "suggest": "Suggest", @@ -1222,6 +1233,7 @@ "support": "Support", "support-url": "Support URL", "supported-language-plural": "Supported Languages", + "sync-entity": "Sync Alert", "synonym-lowercase-plural": "synonyms", "synonym-plural": "Synonyms", "table": "Table", @@ -1309,6 +1321,7 @@ "total": "Total", "total-entity": "Total {{entity}}", "total-index-sent": " Total index sent", + "total-unprocessed-event-plural": "Total Unprocessed Events", "total-user-plural": "Total Users", "tour": "Tour", "tracking": "Tracking", @@ -1417,7 +1430,7 @@ "add-kpi-message": "Identify the Key Performance Indicators (KPI) that best reflect the health of your data assets. Review your data assets based on Description, Ownership, and Tier. Define your target metrics in absolute or percentage to track your progress. Finally, set a start and end date to achieve your data goals.", "add-new-service-description": "Choose from the range of services that OpenMetadata integrates with. To add a new service, start by selecting a Service Category (Database, Messaging, Dashboard, or Pipeline). From the list of available services, select the one you'd want to integrate with.", "add-policy-message": "Policies are assigned to teams. In OpenMetadata, a policy is a collection of rules, which define access based on certain conditions. We support rich SpEL (Spring Expression Language) based conditions. All the operations supported by an entity are published. Use these fine grained operations to define the conditional rules for each policy. Create well-defined policies based on conditional rules to build rich access control roles.", - "add-query-helper-message": "Add a SQL query to execute in the database. The same query can be added to multiple tables by selecting from the tables in the option ‘Query used in’. Choose to describe your query for future reference.", + "add-query-helper-message": "Add a SQL query to execute in the database. The same query can be added to multiple tables by selecting from the tables in the option 'Query used in'. Choose to describe your query for future reference.", "add-role-message": "Roles are assigned to Users. In OpenMetadata, Roles are a collection of Policies. Each Role must have at least one policy attached to it. A Role supports multiple policies with a one to many relationship. Ensure that the necessary policies are created before creating a new role. Build rich access control roles with well-defined policies based on conditional rules.", "adding-new-asset-to-team": "Click on Add Asset, which will take you to the Explore page. From there, you'll be able to assign a Team as the Owner of the asset.", "adding-new-entity-is-easy-just-give-it-a-spin": "Adding a new {{entity}} is easy, just give it a spin!", @@ -1485,7 +1498,7 @@ "configure-webhook-name-message": "OpenMetadata can be configured to automatically send out event notifications to registered {{webhookType}} webhooks through OpenMetadata. Enter the {{webhookType}} webhook name, and an Endpoint URL to receive the HTTP callback on. Use Event Filters to only receive notifications for the required entities. Filter events based on when an entity is created, updated, or deleted. Add a description to note the use case of the webhook. You can use advanced configuration to set up a shared secret key to verify the {{webhookType}} webhook events using HMAC signature.", "configured-sso-provider-is-not-supported": "The configured SSO Provider \"{{provider}}\" is not supported. Please check the authentication configuration in the server.", "confirm-delete-message": "Are you sure you want to permanently delete this message?", - "connection-details-description": "Every service comes with its standard set of requirements and here are the basics of what you’d need to connect. The connection requirements are generated from the JSON schema for that service. The mandatory fields are marked with an asterisk.", + "connection-details-description": "Every service comes with its standard set of requirements and here are the basics of what you'd need to connect. The connection requirements are generated from the JSON schema for that service. The mandatory fields are marked with an asterisk.", "connection-test-failed": "Test connection failed, please validate your connection and permissions for the failed steps.", "connection-test-successful": "Connection test was successful.", "connection-test-warning": "Test connection partially successful: Some steps had failures, we will only ingest partial metadata.", @@ -1595,7 +1608,7 @@ "entity-saved-successfully": "{{entity}} saved successfully", "entity-size-in-between": "{{entity}} size must be between {{min}} and {{max}}", "entity-size-must-be-between-2-and-64": "{{entity}} size must be between 2 and 64", - "entity-transfer-message": "Click on Confirm if you’d like to move <0>{{from}} {{entity}} under <0>{{to}} {{entity}}.", + "entity-transfer-message": "Click on Confirm if you'd like to move <0>{{from}} {{entity}} under <0>{{to}} {{entity}}.", "enum-property-update-message": "Enum values update started. You will be notified once it's done.", "enum-with-description-update-note": "Updating existing value keys is not allowed; only the description can be edited. However, adding new values is allowed.", "error-self-signup-disabled": "Self-signup is currently disabled. To proceed, please reach out to your administrator for further assistance or to request access.", @@ -1701,7 +1714,7 @@ "name-of-the-bucket-dbt-files-stored": "Name of the bucket where the dbt files are stored.", "new-conversation": "You are starting a new conversation", "new-to-the-platform": "New to the platform?", - "no-access-placeholder": "You don’t have access, please check with the admin to get permissions", + "no-access-placeholder": "You don't have access, please check with the admin to get permissions", "no-activity-feed": "Right now, there are no updates in the data assets you own or follow. <0>{{explored}} Dive in and claim ownership or follow the data assets that interest you to stay informed about their latest activities!", "no-announcement-message": "No Announcements, Click on add announcement to add one.", "no-asset-available": "No assets available.", @@ -1889,7 +1902,7 @@ "service-created-entity-description": "The has been created successfully. Visit the newly created service to take a look at the details. {{entity}}", "service-description": "Set up connectors and ingest metadata from diverse sources", "service-name-length": "Service name length must be between 1 and 128 characters", - "service-requirements-description": "Every service comes with its standard set of requirements and here are the basics of what you’d need to connect.", + "service-requirements-description": "Every service comes with its standard set of requirements and here are the basics of what you'd need to connect.", "service-with-delimiters-not-allowed": "Service name with delimiters are not allowed", "service-with-space-not-allowed": "Service name with spaces are not allowed", "session-expired": "Your session has timed out! Please sign in again to access OpenMetadata.", @@ -1906,7 +1919,7 @@ "star-on-github-description": "Hey developers, let's supercharge OpenMetadata's Open Source scene! 🚀 Your Stars can rocket us to the top as the go-to metadata platform. Spread the word, make some noise, and let's get OpenMetadata on everyone's radar! 🌟", "still-running-into-issue": "If you are still running into issues, please reach out to us on slack.", "success-status-for-entity-deploy": "<0>{{entity}} has been {{entityStatus}} and deployed successfully", - "successfully-completed-the-tour": "You’ve successfully completed the tour.", + "successfully-completed-the-tour": "You've successfully completed the tour.", "synonym-placeholder": "To add a synonym, simply type it in and press Enter", "system-alert-edit-message": "Editing a system generated alert is not allowed.", "system-tag-delete-disable-message": "Deleting a system generated tags is not allowed. You can try disabling the tag instead.", diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/Alerts.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/Alerts.mock.ts index a02008ec0d3b..9423066ab9d0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/mocks/Alerts.mock.ts +++ b/openmetadata-ui/src/main/resources/ui/src/mocks/Alerts.mock.ts @@ -314,3 +314,25 @@ export const MOCK_TYPED_EVENT_LIST_RESPONSE: PagingResponse = { offset: 0, }, }; + +export const mockDiagnosticData = { + latestOffset: 100, + currentOffset: 80, + startingOffset: 0, + successfulEventsCount: 75, + failedEventsCount: 5, + relevantUnprocessedEventsCount: 10, + totalUnprocessedEventsCount: 20, + hasProcessedAllEvents: true, +}; + +export const mockEmptyDiagnosticData = { + latestOffset: 0, + currentOffset: 0, + startingOffset: 0, + successfulEventsCount: 0, + failedEventsCount: 0, + relevantUnprocessedEventsCount: 0, + totalUnprocessedEventsCount: 0, + hasProcessedAllEvents: false, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx index 466b2ecfaf4c..ff0e9671b03f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx @@ -11,6 +11,7 @@ * limitations under the License. */ +import { SyncOutlined } from '@ant-design/icons'; import { Button, Col, Row, Skeleton, Space, Tabs, Tooltip } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; @@ -21,6 +22,8 @@ import { useHistory, useParams } from 'react-router-dom'; import { ReactComponent as EditIcon } from '../../assets/svg/edit-new.svg'; import { ReactComponent as DeleteIcon } from '../../assets/svg/ic-delete.svg'; import AlertConfigDetails from '../../components/Alerts/AlertDetails/AlertConfigDetails/AlertConfigDetails'; +import AlertDiagnosticInfoTab from '../../components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab'; +import { AlertDiagnosticData } from '../../components/Alerts/AlertDetails/AlertDiagnosticInfo/AlertDiagnosticInfoTab.interface'; import AlertRecentEventsTab from '../../components/Alerts/AlertDetails/AlertRecentEventsTab/AlertRecentEventsTab'; import DeleteWidgetModal from '../../components/common/DeleteWidget/DeleteWidgetModal'; import DescriptionV1 from '../../components/common/EntityDescription/DescriptionV1'; @@ -51,7 +54,9 @@ import { useFqn } from '../../hooks/useFqn'; import { updateNotificationAlert } from '../../rest/alertsAPI'; import { getAlertEventsDiagnosticsInfo, + getDiagnosticInfo, getObservabilityAlertByFQN, + syncOffset, updateObservabilityAlert, } from '../../rest/observabilityAPI'; import { getAlertExtraInfo } from '../../utils/Alerts/AlertsUtil'; @@ -65,7 +70,7 @@ import { getSettingPath, } from '../../utils/RouterUtils'; import searchClassBase from '../../utils/SearchClassBase'; -import { showErrorToast } from '../../utils/ToastUtils'; +import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; import './alert-details-page.less'; import { AlertDetailsPageProps } from './AlertDetailsPage.interface'; @@ -90,6 +95,7 @@ function AlertDetailsPage({ const [alertPermission, setAlertPermission] = useState( DEFAULT_ENTITY_PERMISSION ); + const [diagnosticInfo, setDiagnosticInfo] = useState(); const { viewPermission, @@ -169,6 +175,15 @@ function AlertDetailsPage({ } }; + const fetchDiagnosticInfo = async () => { + try { + const diagnosticInfoData = await getDiagnosticInfo(fqn); + setDiagnosticInfo(diagnosticInfoData); + } catch (error) { + showErrorToast(error as AxiosError); + } + }; + const breadcrumb = useMemo( () => isNotificationAlert @@ -217,6 +232,15 @@ function AlertDetailsPage({ ); }, [history]); + const handleAlertSync = useCallback(async () => { + try { + await syncOffset(fqn); + showSuccessToast(t('label.alert-synced-successfully')); + } catch (error) { + showErrorToast(error as AxiosError); + } + }, [fqn]); + const onOwnerUpdate = useCallback( async (owners?: EntityReference[]) => { try { @@ -283,12 +307,23 @@ function AlertDetailsPage({ ), }, + { + label: t('label.diagnostic-info'), + key: AlertDetailTabs.DIAGNOSTIC_INFO, + children: isUndefined(diagnosticInfo) ? null : ( + + ), + }, ], - [alertDetails, viewPermission] + [alertDetails, viewPermission, diagnosticInfo] ); const handleTabChange = useCallback( (activeKey: string) => { + if (activeKey === AlertDetailTabs.DIAGNOSTIC_INFO) { + fetchDiagnosticInfo(); + } + history.replace( isNotificationAlert ? getNotificationAlertDetailsPath(fqn, activeKey) @@ -379,6 +414,17 @@ function AlertDetailsPage({ + +