diff --git a/openmetadata-service/src/main/resources/json/data/document/announcements.json b/openmetadata-service/src/main/resources/json/data/document/announcements.json deleted file mode 100644 index 0746ea29aa2..00000000000 --- a/openmetadata-service/src/main/resources/json/data/document/announcements.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "Announcements", - "displayName": "Announcements", - "description": "Announcements KnowledgePanel shows the Announcements from teams,users published on Data Assets.", - "entityType": "KnowledgePanel", - "fullyQualifiedName": "KnowledgePanel.Announcements", - "data": { - "gridSizes": ["small"] - } -} \ No newline at end of file diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/docstore/DocStoreResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/docstore/DocStoreResourceTest.java index df486337cfa..9c1d4b17c7b 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/docstore/DocStoreResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/docstore/DocStoreResourceTest.java @@ -148,7 +148,7 @@ public class DocStoreResourceTest extends EntityResourceTest panelList = listEntities(queryParams, ADMIN_AUTH_HEADERS); - assertEquals(panelDocs.size() + 7, panelList.getPaging().getTotal()); + assertEquals(panelDocs.size() + 6, panelList.getPaging().getTotal()); // docs List pageDocs = new ArrayList<>(); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/CustomizeLandingPageUtils.js b/openmetadata-ui/src/main/resources/ui/cypress/common/CustomizeLandingPageUtils.js new file mode 100644 index 00000000000..3f231647da7 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/CustomizeLandingPageUtils.js @@ -0,0 +1,127 @@ +/* + * 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. + */ +// eslint-disable-next-line spaced-comment +/// + +import { + interceptURL, + toastNotification, + verifyResponseStatusCode, +} from './common'; + +export const removeAndCheckWidget = ({ widgetTestId, widgetKey }) => { + // Click on remove widget button + cy.get( + `[data-testid="${widgetTestId}"] [data-testid="remove-widget-button"]` + ).click({ waitForAnimations: true }); + + cy.get(`[data-testid="${widgetTestId}"]`).should('not.exist'); + + // Check if empty widget placeholder is displayed in place of removed widget + cy.get( + `[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"]` + ).should('exist'); + + // Remove empty widget placeholder + cy.get( + `[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"] [data-testid="remove-widget-button"]` + ).click({ waitForAnimations: true }); + cy.get( + `[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"]` + ).should('not.exist'); +}; + +export const navigateToCustomizeLandingPage = ({ + personaName, + customPageDataResponse, +}) => { + interceptURL('GET', '/api/v1/teams/name/*', 'settingsPage'); + + cy.get('[data-testid="app-bar-item-settings"]').click(); + verifyResponseStatusCode('@settingsPage', 200); + cy.get('[data-testid="settings-left-panel"]').should('be.visible'); + + interceptURL('GET', '/api/v1/personas*', 'getPersonas'); + cy.get(`[data-menu-id*="openMetadata.customizeLandingPage"]`) + .scrollIntoView() + .click(); + + verifyResponseStatusCode('@getPersonas', 200); + + interceptURL( + 'GET', + `/api/v1/docStore/name/persona.${personaName}.Page.LandingPage`, + 'getCustomPageData' + ); + interceptURL('GET', `/api/v1/users/*?fields=follows,owns`, 'getMyData'); + + cy.get( + `[data-testid="persona-details-card-${personaName}"] [data-testid="customize-page-button"]` + ).click(); + + verifyResponseStatusCode('@getCustomPageData', customPageDataResponse); + verifyResponseStatusCode('@getMyData', 200); +}; + +export const saveLayout = () => { + // Save layout + interceptURL('PATCH', `/api/v1/docStore/*`, 'getMyData'); + + cy.get('[data-testid="save-button"]').click(); + + verifyResponseStatusCode('@getMyData', 200); + + toastNotification('Page layout updated successfully.'); +}; + +export const navigateToLandingPage = () => { + interceptURL('GET', `/api/v1/feed*`, 'getFeedsData'); + interceptURL( + 'GET', + `/api/v1/analytics/dataInsights/charts/aggregate*`, + 'getDataInsightReport' + ); + + cy.get('#openmetadata_logo').click(); + + verifyResponseStatusCode('@getFeedsData', 200); + verifyResponseStatusCode('@getDataInsightReport', 200); +}; + +export const openAddWidgetModal = () => { + interceptURL( + 'GET', + `/api/v1/docStore?fqnPrefix=KnowledgePanel`, + 'getWidgetsList' + ); + + cy.get( + '[data-testid="ExtraWidget.EmptyWidgetPlaceholder"] [data-testid="add-widget-button"]' + ).click(); + + verifyResponseStatusCode('@getWidgetsList', 200); +}; + +export const checkAllWidgets = (checkEmptyWidgetPlaceholder = false) => { + cy.get('[data-testid="activity-feed-widget"]').should('exist'); + cy.get('[data-testid="following-widget"]').should('exist'); + cy.get('[data-testid="recently-viewed-widget"]').should('exist'); + cy.get('[data-testid="my-data-widget"]').should('exist'); + cy.get('[data-testid="kpi-widget"]').should('exist'); + cy.get('[data-testid="total-assets-widget"]').should('exist'); + if (checkEmptyWidgetPlaceholder) { + cy.get('[data-testid="ExtraWidget.EmptyWidgetPlaceholder"]').should( + 'exist' + ); + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/constants/EntityConstant.js b/openmetadata-ui/src/main/resources/ui/cypress/constants/EntityConstant.js index 6e7f0053e99..6192dc5c947 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/constants/EntityConstant.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/EntityConstant.js @@ -436,3 +436,18 @@ export const VISIT_ENTITIES_DATA = { serviceName: STORAGE_SERVICE.service.name, }, }; + +export const USER_NAME = `user${uuid()}`; + +export const USER_DETAILS = { + firstName: `first-name-${uuid()}`, + lastName: `last-name-${uuid()}`, + email: `${USER_NAME}@example.com`, + password: 'User@OMD123', +}; + +export const PERSONA_DETAILS = { + name: `persona-${uuid()}`, + displayName: `persona ${uuid()}`, + description: `Persona description.`, +}; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/Following.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/Following.spec.js index 60ca1ded627..305e3bd85b7 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/Following.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/Following.spec.js @@ -137,16 +137,17 @@ describe('Following data assets', () => { }); it('following section should be present', () => { - cy.get('[data-testid="following-data-container"]') + cy.get('[data-testid="following-widget"]') .scrollIntoView() .should('be.visible'); - cy.get('[data-testid="following-data-container"]').contains( + cy.get('[data-testid="following-widget"]').contains( 'You have not followed anything yet.' ); - cy.get( - `[data-testid="following-data-container"] .right-panel-list-item` - ).should('have.length', 0); + cy.get(`[data-testid="following-widget"] .right-panel-list-item`).should( + 'have.length', + 0 + ); }); // Follow entity diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/RecentlyViewed.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/RecentlyViewed.spec.js index a293b34c362..585cbffc35f 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/RecentlyViewed.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/RecentlyViewed.spec.js @@ -96,12 +96,12 @@ describe('Recently viwed data assets', () => { }); it('recently view section should be present', () => { - cy.get('[data-testid="recently-viewed-container"]') + cy.get('[data-testid="recently-viewed-widget"]') .scrollIntoView() .should('be.visible'); cy.get( - `[data-testid="recently-viewed-container"] .right-panel-list-item` + `[data-testid="recently-viewed-widget"] .right-panel-list-item` ).should('have.length', 0); }); @@ -124,18 +124,18 @@ describe('Recently viwed data assets', () => { // need to add manual wait as we are dependant on local storage for recently view data cy.wait(500); - cy.get('[data-testid="recently-viewed-container"]') + cy.get('[data-testid="recently-viewed-widget"]') .scrollIntoView() .should('be.visible'); cy.get( - `[data-testid="recently-viewed-container"] [title="${entity.displayName}"]` + `[data-testid="recently-viewed-widget"] [title="${entity.displayName}"]` ) .scrollIntoView() .should('be.visible'); // Checking count since we will only show max 5 not more than that cy.get( - `[data-testid="recently-viewed-container"] .right-panel-list-item` + `[data-testid="recently-viewed-widget"] .right-panel-list-item` ).should('have.length', index + 1); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/CustomizeLandingPage.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/CustomizeLandingPage.spec.js new file mode 100644 index 00000000000..64920994fed --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/CustomizeLandingPage.spec.js @@ -0,0 +1,237 @@ +/* + * 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. + */ +// eslint-disable-next-line spaced-comment +/// + +import { compare } from 'fast-json-patch'; +import { interceptURL, toastNotification } from '../../common/common'; +import { + checkAllWidgets, + navigateToCustomizeLandingPage, + navigateToLandingPage, + openAddWidgetModal, + removeAndCheckWidget, + saveLayout, +} from '../../common/CustomizeLandingPageUtils'; +import { PERSONA_DETAILS } from '../../constants/EntityConstant'; + +describe('Customize Landing Page Flow', () => { + let testData = {}; + before(() => { + cy.login(); + cy.getAllLocalStorage().then((data) => { + const token = Object.values(data)[0].oidcIdToken; + + // Fetch logged in user details to get user id + cy.request({ + method: 'GET', + url: `/api/v1/users/loggedInUser`, + headers: { Authorization: `Bearer ${token}` }, + }).then((userResponse) => { + // Create a persona + cy.request({ + method: 'POST', + url: `/api/v1/personas`, + headers: { Authorization: `Bearer ${token}` }, + body: { ...PERSONA_DETAILS, users: [userResponse.body.id] }, + }).then((personaResponse) => { + testData.user = userResponse.body; + testData.persona = personaResponse.body; + const { + name, + id, + description, + displayName, + fullyQualifiedName, + href, + } = personaResponse.body; + + // Set newly created persona as default persona for the logged in user + const patchData = compare(userResponse.body, { + ...userResponse.body, + defaultPersona: { + name, + id, + description, + displayName, + fullyQualifiedName, + href, + type: 'persona', + }, + }); + + cy.request({ + method: 'PATCH', + url: `/api/v1/users/${testData.user.id}`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json-patch+json', + }, + body: patchData, + }); + }); + }); + }); + }); + + after(() => { + cy.login(); + const token = localStorage.getItem('oidcIdToken'); + + // Delete created user + cy.request({ + method: 'DELETE', + url: `/api/v1/personas/${testData.persona.id}`, + headers: { Authorization: `Bearer ${token}` }, + }); + + // Delete created landing page config doc + cy.request({ + method: 'DELETE', + url: `/api/v1/docStore/${testData.docStoreData.id}`, + headers: { Authorization: `Bearer ${token}` }, + }); + }); + + beforeEach(() => { + cy.login(); + }); + + it('Creation of custom landing page config and widget removal should work properly', () => { + navigateToCustomizeLandingPage({ + personaName: PERSONA_DETAILS.name, + customPageDataResponse: 404, + }); + + checkAllWidgets(true); + + // Editing the layout + removeAndCheckWidget({ + widgetTestId: 'activity-feed-widget', + widgetKey: 'KnowledgePanel.ActivityFeed', + }); + removeAndCheckWidget({ + widgetTestId: 'following-widget', + widgetKey: 'KnowledgePanel.Following', + }); + removeAndCheckWidget({ + widgetTestId: 'kpi-widget', + widgetKey: 'KnowledgePanel.KPI', + }); + + // Save layout + interceptURL('POST', `/api/v1/docStore`, 'getMyData'); + + cy.get('[data-testid="save-button"]').click(); + + cy.wait('@getMyData').then((interception) => { + testData.docStoreData = interception.response.body; + + expect(interception.response.statusCode).equal(201); + }); + + toastNotification('Page layout created successfully.'); + + navigateToLandingPage(); + + // Check if removed widgets are not present on landing page + cy.get(`[data-testid="activity-feed-widget"]`).should('not.exist'); + cy.get(`[data-testid="following-widget"]`).should('not.exist'); + cy.get(`[data-testid="kpi-widget"]`).should('not.exist'); + }); + + it('Adding new widget should work properly', () => { + navigateToCustomizeLandingPage({ + personaName: PERSONA_DETAILS.name, + customPageDataResponse: 200, + }); + + // Check if removed widgets are not present on customize page + cy.get('[data-testid="activity-feed-widget"]').should('not.exist'); + cy.get('[data-testid="following-widget"]').should('not.exist'); + cy.get('[data-testid="kpi-widget"]').should('not.exist'); + + // Check if other widgets are present + cy.get('[data-testid="recently-viewed-widget"]').should('exist'); + cy.get('[data-testid="my-data-widget"]').should('exist'); + cy.get('[data-testid="total-assets-widget"]').should('exist'); + cy.get('[data-testid="ExtraWidget.EmptyWidgetPlaceholder"]').should( + 'exist' + ); + + openAddWidgetModal(); + + // Check if 'check' icon is present for existing widgets + cy.get('[data-testid="MyData-check-icon"]').should('exist'); + cy.get('[data-testid="RecentlyViewed-check-icon"]').should('exist'); + cy.get('[data-testid="TotalAssets-check-icon"]').should('exist'); + + // Check if 'check' icon is not present for removed widgets + cy.get('[data-testid="ActivityFeed-check-icon"]').should('not.exist'); + cy.get('[data-testid="Following-check-icon"]').should('not.exist'); + cy.get('[data-testid="KPI-check-icon"]').should('not.exist'); + + // Add Following widget + cy.get('[data-testid="Following-widget-tab-label"]').click(); + cy.get( + '[aria-labelledby$="KnowledgePanel.Following"] [data-testid="add-widget-button"]' + ).click(); + cy.get('[data-testid="following-widget"]').should('exist'); + + // Check if check icons are present in tab labels for newly added widgets + openAddWidgetModal(); + cy.get('[data-testid="Following-check-icon"]').should('exist'); + cy.get('[data-testid="add-widget-modal"] [aria-label="Close"]').click(); + + saveLayout(); + + navigateToLandingPage(); + + cy.get(`[data-testid="activity-feed-widget"]`).should('not.exist'); + cy.get(`[data-testid="kpi-widget"]`).should('not.exist'); + + // Check if newly added widgets are present on landing page + cy.get(`[data-testid="following-widget"]`).should('exist'); + }); + + it('Resetting the layout flow should work properly', () => { + // Check if removed widgets are not present on landing page + cy.get(`[data-testid="activity-feed-widget"]`).should('not.exist'); + cy.get(`[data-testid="kpi-widget"]`).should('not.exist'); + + navigateToCustomizeLandingPage({ + personaName: PERSONA_DETAILS.name, + customPageDataResponse: 200, + }); + + // Check if removed widgets are not present on customize page + cy.get(`[data-testid="activity-feed-widget"]`).should('not.exist'); + cy.get(`[data-testid="kpi-widget"]`).should('not.exist'); + + cy.get(`[data-testid="reset-button"]`).click(); + + cy.get(`[data-testid="reset-layout-modal"] .ant-modal-footer`) + .contains('Yes') + .click(); + + toastNotification('Page layout updated successfully.'); + + // Check if all widgets are present after resetting the layout + checkAllWidgets(true); + + // Check if all widgets are present on landing page + navigateToLandingPage(); + + checkAllWidgets(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/PersonaFlow.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/PersonaFlow.spec.js new file mode 100644 index 00000000000..228b2e2e9f1 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/PersonaFlow.spec.js @@ -0,0 +1,283 @@ +/* + * Copyright 2022 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. + */ +// eslint-disable-next-line spaced-comment +/// + +import { + descriptionBox, + interceptURL, + toastNotification, + verifyResponseStatusCode, +} from '../../common/common'; +import { DELETE_TERM } from '../../constants/constants'; +import { PERSONA_DETAILS, USER_DETAILS } from '../../constants/EntityConstant'; + +const updatePersonaDisplayName = (displayName) => { + interceptURL('PATCH', `/api/v1/personas/*`, 'updatePersona'); + + cy.get('[data-testid="manage-button"]').click(); + + cy.get( + '[data-testid="manage-dropdown-list-container"] [data-testid="rename-button"]' + ).click(); + + cy.get('#name').should('be.disabled'); + cy.get('#displayName').should('not.be.disabled').clear(); + + cy.get('#displayName').type(displayName); + + cy.get('[data-testid="save-button"]').click(); + verifyResponseStatusCode('@updatePersona', 200); +}; + +describe('Persona operations', () => { + let user = {}; + const userSearchText = `${USER_DETAILS.firstName}${USER_DETAILS.lastName}`; + before(() => { + cy.login(); + cy.getAllLocalStorage().then((data) => { + const token = Object.values(data)[0].oidcIdToken; + + // Create a new user + cy.request({ + method: 'POST', + url: `/api/v1/users/signup`, + headers: { Authorization: `Bearer ${token}` }, + body: USER_DETAILS, + }).then((response) => { + user.details = response.body; + }); + }); + }); + + after(() => { + cy.login(); + cy.getAllLocalStorage().then((data) => { + const token = Object.values(data)[0].oidcIdToken; + + // Delete created user + cy.request({ + method: 'DELETE', + url: `/api/v1/users/${user.details.id}?hardDelete=true&recursive=false`, + headers: { Authorization: `Bearer ${token}` }, + }); + }); + }); + + beforeEach(() => { + cy.login(); + interceptURL('GET', '/api/v1/teams/name/*', 'settingsPage'); + + cy.get('[data-testid="app-bar-item-settings"]').click(); + verifyResponseStatusCode('@settingsPage', 200); + cy.get('[data-testid="settings-left-panel"]').should('be.visible'); + + interceptURL('GET', '/api/v1/personas*', 'getPersonas'); + cy.get(`[data-menu-id*="members.persona"]`).scrollIntoView().click(); + + verifyResponseStatusCode('@getPersonas', 200); + }); + + it('Persona creation should work properly', () => { + cy.get('[data-testid="add-persona-button"]').scrollIntoView().click(); + cy.get('[data-testid="name"]').clear().type(PERSONA_DETAILS.name); + cy.get('[data-testid="displayName"]') + .clear() + .type(PERSONA_DETAILS.displayName); + cy.get(descriptionBox).type(PERSONA_DETAILS.description); + cy.get('[data-testid="add-users"]').scrollIntoView().click(); + + cy.get('[data-testid="searchbar"]').type(userSearchText); + + cy.get(`[title="${userSearchText}"] .ant-checkbox-input`).check(); + cy.get('[data-testid="selectable-list-update-btn"]') + .scrollIntoView() + .click(); + + interceptURL('POST', '/api/v1/personas', 'createPersona'); + + cy.get('.ant-modal-footer > .ant-btn-primary') + .contains('Create') + .scrollIntoView() + .click(); + + verifyResponseStatusCode('@createPersona', 201); + + // Verify created persona details + + cy.get('[data-testid="persona-details-card"] .ant-card-meta-title').should( + 'contain', + PERSONA_DETAILS.displayName + ); + cy.get( + '[data-testid="persona-details-card"] .ant-card-meta-description' + ).should('contain', PERSONA_DETAILS.description); + + interceptURL( + 'GET', + `/api/v1/personas/name/${PERSONA_DETAILS.name}*`, + 'getPersonaDetails' + ); + + cy.get('[data-testid="persona-details-card"]') + .contains(PERSONA_DETAILS.displayName) + .scrollIntoView() + .click(); + + verifyResponseStatusCode('@getPersonaDetails', 200); + + cy.get( + '[data-testid="page-header-container"] [data-testid="heading"]' + ).should('contain', PERSONA_DETAILS.displayName); + cy.get( + '[data-testid="page-header-container"] [data-testid="sub-heading"]' + ).should('contain', PERSONA_DETAILS.name); + cy.get( + '[data-testid="viewer-container"] [data-testid="markdown-parser"]' + ).should('contain', PERSONA_DETAILS.description); + + cy.get( + `[data-row-key="${user.details.name}"] [data-testid="${user.details.name}"]` + ).should('contain', user.details.name); + }); + + it('Persona update description flow should work properly', () => { + interceptURL( + 'GET', + `/api/v1/personas/name/${PERSONA_DETAILS.name}*`, + 'getPersonaDetails' + ); + + cy.get('[data-testid="persona-details-card"]') + .contains(PERSONA_DETAILS.displayName) + .scrollIntoView() + .click(); + + verifyResponseStatusCode('@getPersonaDetails', 200); + + cy.get('[data-testid="edit-description"]').click(); + + cy.get(`[data-testid="markdown-editor"] ${descriptionBox}`) + .clear() + .type('Updated description.'); + + interceptURL('PATCH', `/api/v1/personas/*`, 'updatePersona'); + + cy.get(`[data-testid="markdown-editor"] [data-testid="save"]`).click(); + + verifyResponseStatusCode('@updatePersona', 200); + + cy.get( + `[data-testid="viewer-container"] [data-testid="markdown-parser"]` + ).should('contain', 'Updated description.'); + }); + + it('Persona rename flow should work properly', () => { + interceptURL( + 'GET', + `/api/v1/personas/name/${PERSONA_DETAILS.name}*`, + 'getPersonaDetails' + ); + + cy.get('[data-testid="persona-details-card"]') + .contains(PERSONA_DETAILS.displayName) + .scrollIntoView() + .click(); + + verifyResponseStatusCode('@getPersonaDetails', 200); + + updatePersonaDisplayName('Test Persona'); + + cy.get('[data-testid="heading"]').should('contain', 'Test Persona'); + + updatePersonaDisplayName(PERSONA_DETAILS.displayName); + + cy.get('[data-testid="heading"]').should( + 'contain', + PERSONA_DETAILS.displayName + ); + }); + + it('Remove users in persona should work properly', () => { + // Remove user from the users tab + interceptURL( + 'GET', + `/api/v1/personas/name/${PERSONA_DETAILS.name}*`, + 'getPersonaDetails' + ); + + cy.get('[data-testid="persona-details-card"]') + .contains(PERSONA_DETAILS.displayName) + .scrollIntoView() + .click(); + + verifyResponseStatusCode('@getPersonaDetails', 200); + + cy.get( + `[data-row-key="${user.details.name}"] [data-testid="remove-user-btn"]` + ).click(); + + cy.get('[data-testid="remove-confirmation-modal"]').should( + 'contain', + `Are you sure you want to remove ${user.details.name}?` + ); + + interceptURL('PATCH', `/api/v1/personas/*`, 'updatePersona'); + + cy.get('[data-testid="remove-confirmation-modal"]') + .contains('Confirm') + .click(); + + verifyResponseStatusCode('@updatePersona', 200); + }); + + it('Delete persona should work properly', () => { + interceptURL( + 'GET', + `/api/v1/personas/name/${PERSONA_DETAILS.name}*`, + 'getPersonaDetails' + ); + + cy.get('[data-testid="persona-details-card"]') + .contains(PERSONA_DETAILS.displayName) + .scrollIntoView() + .click(); + + verifyResponseStatusCode('@getPersonaDetails', 200); + + cy.get('[data-testid="manage-button"]').click(); + + cy.get('[data-testid="delete-button-title"]').click(); + + cy.get('.ant-modal-header').should( + 'contain', + `Delete ${PERSONA_DETAILS.name}` + ); + + cy.get(`[data-testid="hard-delete-option"]`).click(); + + cy.get('[data-testid="confirm-button"]').should('be.disabled'); + cy.get('[data-testid="confirmation-text-input"]').type(DELETE_TERM); + + interceptURL( + 'DELETE', + `/api/v1/personas/*?hardDelete=true&recursive=false`, + `deletePersona` + ); + cy.get('[data-testid="confirm-button"]').should('not.be.disabled'); + cy.get('[data-testid="confirm-button"]').click(); + verifyResponseStatusCode(`@deletePersona`, 200); + + toastNotification(`Persona deleted successfully!`); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx index 13209411631..e2117aa0d6e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx @@ -32,8 +32,8 @@ const GlobalSettingPage = withSuspenseFallback( React.lazy(() => import('../../pages/GlobalSettingPage/GlobalSettingPage')) ); -const MyDataPageV1 = withSuspenseFallback( - React.lazy(() => import('../../pages/MyDataPage/MyDataPageV1.component')) +const MyDataPage = withSuspenseFallback( + React.lazy(() => import('../../pages/MyDataPage/MyDataPage.component')) ); const TestSuiteIngestionPage = withSuspenseFallback( @@ -390,7 +390,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => { return ( - + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx index e7f93d271ec..50058029d6b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx @@ -44,7 +44,7 @@ import { REDIRECT_PATHNAME, ROUTES, } from '../../../constants/constants'; -import { ClientErrors } from '../../../enums/axios.enum'; +import { ClientErrors } from '../../../enums/Axios.enum'; import { AuthenticationConfiguration } from '../../../generated/configuration/authenticationConfiguration'; import { AuthorizerConfiguration } from '../../../generated/configuration/authorizerConfiguration'; import { User } from '../../../generated/entity/teams/user'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.interface.ts index 9f6aef92e44..52f306f9051 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.interface.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { ReactNode } from 'react'; import { Document } from '../../../generated/entity/docStore/document'; export interface AddWidgetModalProps { @@ -34,6 +35,6 @@ export interface AddWidgetTabContentProps { } export interface WidgetSizeInfo { - label: string; + label: ReactNode; value: number; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.test.tsx new file mode 100644 index 00000000000..dec77d63896 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.test.tsx @@ -0,0 +1,156 @@ +/* + * Copyright 2023 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 { act, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { mockWidgetsData } from '../../../mocks/AddWidgetModal.mock'; +import { getAllKnowledgePanels } from '../../../rest/DocStoreAPI'; +import AddWidgetModal from './AddWidgetModal'; +import { AddWidgetModalProps } from './AddWidgetModal.interface'; + +const mockProps: AddWidgetModalProps = { + open: true, + addedWidgetsList: [], + handleCloseAddWidgetModal: jest.fn(), + handleAddWidget: jest.fn(), + maxGridSizeSupport: 4, + placeholderWidgetKey: 'placeholderKey', +}; + +jest.mock('../../Loader/Loader', () => + jest.fn().mockImplementation(() =>
Loader
) +); + +jest.mock('../../common/ErrorWithPlaceholder/ErrorPlaceHolder', () => + jest.fn().mockImplementation(() =>
ErrorPlaceHolder
) +); + +jest.mock('./AddWidgetTabContent', () => + jest.fn().mockImplementation(({ getAddWidgetHandler }) => ( +
+ AddWidgetTabContent +
+ getAddWidgetHandler +
+
+ )) +); + +jest.mock('../../../utils/ToastUtils', () => ({ + showErrorToast: jest.fn(), +})); + +jest.mock('../../../rest/DocStoreAPI', () => ({ + getAllKnowledgePanels: jest + .fn() + .mockImplementation(() => Promise.resolve(mockWidgetsData)), +})); + +jest.mock('../../../utils/CustomizableLandingPageUtils', () => ({ + getWidgetWidthLabelFromKey: jest.fn().mockImplementation((label) => label), +})); + +describe('AddWidgetModal component', () => { + it('AddWidgetModal should not display the modal when open is false', async () => { + await act(async () => { + render(); + }); + + expect(screen.queryByTestId('add-widget-modal')).toBeNull(); + }); + + it('AddWidgetModal should display all widgets tab from the widgets list', async () => { + await act(async () => { + render(); + }); + + expect( + screen.getByTestId('ActivityFeed-widget-tab-label') + ).toBeInTheDocument(); + expect( + screen.getByTestId('Following-widget-tab-label') + ).toBeInTheDocument(); + expect(screen.getByTestId('KPI-widget-tab-label')).toBeInTheDocument(); + expect(screen.getByTestId('MyData-widget-tab-label')).toBeInTheDocument(); + expect( + screen.getByTestId('RecentlyViewed-widget-tab-label') + ).toBeInTheDocument(); + expect( + screen.getByTestId('TotalAssets-widget-tab-label') + ).toBeInTheDocument(); + expect(screen.getByText('AddWidgetTabContent')).toBeInTheDocument(); + }); + + it('AddWidgetModal should display check icons in the tab labels only for the tabs included in addedWidgetsList', async () => { + await act(async () => { + render( + + ); + }); + + expect(screen.getByTestId('ActivityFeed-check-icon')).toBeInTheDocument(); + expect(screen.getByTestId('Following-check-icon')).toBeInTheDocument(); + expect(screen.queryByTestId('KPI-check-icon')).toBeNull(); + expect(screen.queryByTestId('MyData-check-icon')).toBeNull(); + expect(screen.queryByTestId('RecentlyViewed-check-icon')).toBeNull(); + expect(screen.queryByTestId('TotalAssets-check-icon')).toBeNull(); + }); + + it('AddWidgetModal should call handleAddWidget when clicked on add widget button', async () => { + await act(async () => { + render(); + }); + + expect(mockProps.handleAddWidget).toHaveBeenCalledTimes(0); + + const addWidgetButton = screen.getByText('getAddWidgetHandler'); + + expect(addWidgetButton).toBeInTheDocument(); + + await act(async () => userEvent.click(addWidgetButton)); + + expect(mockProps.handleAddWidget).toHaveBeenCalledTimes(1); + expect(mockProps.handleAddWidget).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'ActivityFeed', + }), + 'placeholderKey', + 3 + ); + }); + + it('AddWidgetModal should display ErrorPlaceHolder when API to fetch widgets list is failed', async () => { + (getAllKnowledgePanels as jest.Mock).mockImplementation(() => + Promise.reject(new Error('API Failed')) + ); + + await act(async () => { + render(); + }); + + expect(screen.getByText('ErrorPlaceHolder')).toBeInTheDocument(); + }); + + it('AddWidgetModal should', async () => { + await act(async () => { + render(); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.tsx index 4e3b8679985..a38c10800ef 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetModal.tsx @@ -24,6 +24,7 @@ import { getAllKnowledgePanels } from '../../../rest/DocStoreAPI'; import { getWidgetWidthLabelFromKey } from '../../../utils/CustomizableLandingPageUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; +import Loader from '../../Loader/Loader'; import './add-widget-modal.less'; import { AddWidgetModalProps, @@ -41,9 +42,11 @@ function AddWidgetModal({ }: Readonly) { const { t } = useTranslation(); const [widgetsList, setWidgetsList] = useState>(); + const [loading, setLoading] = useState(true); const fetchKnowledgePanels = useCallback(async () => { try { + setLoading(true); const response = await getAllKnowledgePanels({ fqnPrefix: 'KnowledgePanel', }); @@ -51,6 +54,8 @@ function AddWidgetModal({ setWidgetsList(response.data); } catch (error) { showErrorToast(error as AxiosError); + } finally { + setLoading(false); } }, []); @@ -65,19 +70,24 @@ function AddWidgetModal({ widgetsList?.map((widget) => { const widgetSizeOptions: Array = widget.data.gridSizes.map((size: WidgetWidths) => ({ - label: getWidgetWidthLabelFromKey(toString(size)), + label: ( + + {getWidgetWidthLabelFromKey(toString(size))} + + ), value: WidgetWidths[size], })); return { label: ( - + {widget.name} {addedWidgetsList.some((w) => w.startsWith(widget.fullyQualifiedName) ) && ( )} @@ -101,24 +111,42 @@ function AddWidgetModal({ fetchKnowledgePanels(); }, []); + const widgetsInfo = useMemo(() => { + if (loading) { + return ; + } + + if (isEmpty(widgetsList)) { + return ( + + {t('message.no-widgets-to-add')} + + ); + } + + return ( + + ); + }, [loading, widgetsList, tabItems]); + return ( - {isEmpty(widgetsList) ? ( - - {t('message.no-widgets-to-add')} - - ) : ( - - )} + {widgetsInfo} ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.test.tsx new file mode 100644 index 00000000000..3535d606476 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright 2023 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 { act, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { + mockWidget, + mockWidgetSizes, +} from '../../../mocks/AddWidgetTabContent.mock'; +import { AddWidgetTabContentProps } from './AddWidgetModal.interface'; +import AddWidgetTabContent from './AddWidgetTabContent'; + +const mockProps: AddWidgetTabContentProps = { + getAddWidgetHandler: jest.fn(), + maxGridSizeSupport: 4, + widget: mockWidget, + widgetSizeOptions: mockWidgetSizes, +}; + +jest.mock('../../../utils/CustomizePageClassBase', () => ({ + getWidgetImageFromKey: jest.fn().mockImplementation(() => ''), +})); + +describe('AddWidgetTabContent component', () => { + it('AddWidgetTabContent should render properly', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByTestId('size-selector-button')).toBeInTheDocument(); + expect(screen.getByTestId('widget-image')).toBeInTheDocument(); + expect(screen.getByTestId('widget-description')).toBeInTheDocument(); + expect(screen.getByTestId('add-widget-button')).toBeInTheDocument(); + }); + + it('AddWidgetTabContent should display correct size selector buttons', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByText('Small')).toBeInTheDocument(); + expect(screen.getByText('Medium')).toBeInTheDocument(); + expect(screen.queryByText('Large')).toBeNull(); + }); + + it('AddWidgetTabContent should send selected widget size to getAddWidgetHandler', async () => { + await act(async () => { + render(); + }); + + expect(mockProps.getAddWidgetHandler).toHaveBeenCalledTimes(1); + expect(mockProps.getAddWidgetHandler).toHaveBeenCalledWith( + expect.objectContaining(mockProps.widget), + 1 + ); + + const mediumButton = screen.getByText('Medium'); + + await act(async () => userEvent.click(mediumButton)); + + expect(mockProps.getAddWidgetHandler).toHaveBeenCalledTimes(2); + expect(mockProps.getAddWidgetHandler).toHaveBeenCalledWith( + expect.objectContaining(mockProps.widget), + 2 + ); + }); + + it('AddWidgetTabContent should disable the add widget button if widget size exceeds the maxGridSizeSupport', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByTestId('add-widget-button')).toBeDisabled(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.tsx index d061a6045b3..52231024309 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/AddWidgetModal/AddWidgetTabContent.tsx @@ -63,6 +63,7 @@ function AddWidgetTabContent({ {`${t('label.size')}:`} - - + + {widget.description} } type="primary" diff --git a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/CustomizeMyData/CustomizeMyData.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/CustomizeMyData/CustomizeMyData.test.tsx new file mode 100644 index 00000000000..07536138886 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/CustomizeMyData/CustomizeMyData.test.tsx @@ -0,0 +1,281 @@ +/* + * Copyright 2023 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 { act, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { PageType } from '../../../generated/system/ui/page'; +import { + mockActiveAnnouncementData, + mockCustomizePageClassBase, + mockDefaultLayout, + mockDocumentData, + mockPersonaName, + mockUserData, +} from '../../../mocks/MyDataPage.mock'; +import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; +import CustomizeMyData from './CustomizeMyData'; +import { CustomizeMyDataProps } from './CustomizeMyData.interface'; + +const mockPush = jest.fn(); + +const mockProps: CustomizeMyDataProps = { + initialPageData: mockDocumentData, + onSaveLayout: jest.fn(), + handlePageDataChange: jest.fn(), + handleSaveCurrentPageLayout: jest.fn(), +}; + +jest.mock( + '../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider', + () => { + return jest + .fn() + .mockImplementation(({ children }) => ( +
{children}
+ )); + } +); + +jest.mock('../AddWidgetModal/AddWidgetModal', () => { + return jest.fn().mockImplementation(({ handleCloseAddWidgetModal }) => ( +
+ AddWidgetModal +
handleCloseAddWidgetModal
+
+ )); +}); + +jest.mock('../EmptyWidgetPlaceholder/EmptyWidgetPlaceholder', () => { + return jest.fn().mockImplementation(({ handleOpenAddWidgetModal }) => ( +
+ EmptyWidgetPlaceholder +
handleOpenAddWidgetModal
+
+ )); +}); + +jest.mock('../../../utils/CustomizePageClassBase', () => { + return mockCustomizePageClassBase; +}); + +jest.mock('../../PageLayoutV1/PageLayoutV1', () => { + return jest.fn().mockImplementation(({ children, header }) => ( +
+
{header}
+ {children} +
+ )); +}); + +jest.mock('../../Auth/AuthProviders/AuthProvider', () => ({ + useAuthContext: jest + .fn() + .mockImplementation(() => ({ currentUser: mockUserData })), +})); + +jest.mock('../../../rest/feedsAPI', () => ({ + getActiveAnnouncement: jest + .fn() + .mockImplementation(() => mockActiveAnnouncementData), +})); + +jest.mock('../../../rest/userAPI', () => ({ + getUserById: jest.fn().mockImplementation(() => mockUserData), +})); + +jest.mock('react-router-dom', () => ({ + useLocation: jest.fn().mockImplementation(() => ({ pathname: '' })), + useHistory: jest.fn().mockImplementation(() => ({ + push: mockPush, + })), + useParams: jest.fn().mockImplementation(() => ({ + fqn: mockPersonaName, + pageFqn: PageType.LandingPage, + })), + Link: jest.fn().mockImplementation(() =>
Link
), +})); + +jest.mock('react-grid-layout', () => ({ + WidthProvider: jest + .fn() + .mockImplementation(() => + jest + .fn() + .mockImplementation(({ children }) => ( +
{children}
+ )) + ), + __esModule: true, + default: '', +})); + +jest.mock('../../../hooks/authHooks', () => ({ + useAuth: jest.fn().mockImplementation(() => ({ isAuthDisabled: false })), +})); + +describe('CustomizeMyData component', () => { + it('CustomizeMyData should render the widgets in the page config', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByText('KnowledgePanel.ActivityFeed')).toBeInTheDocument(); + expect(screen.getByText('KnowledgePanel.Following')).toBeInTheDocument(); + expect( + screen.getByText('KnowledgePanel.RecentlyViewed') + ).toBeInTheDocument(); + expect(screen.queryByText('KnowledgePanel.Announcements')).toBeNull(); + expect(screen.queryByText('KnowledgePanel.KPI')).toBeNull(); + expect(screen.queryByText('KnowledgePanel.TotalAssets')).toBeNull(); + expect(screen.queryByText('KnowledgePanel.MyData')).toBeNull(); + }); + + it('CustomizeMyData should reroute to the customizable page settings page on click of cancel button', async () => { + await act(async () => { + render(); + }); + + const cancelButton = screen.getByTestId('cancel-button'); + + await act(async () => userEvent.click(cancelButton)); + + expect(mockPush).toHaveBeenCalledWith( + '/settings/openMetadata/customizeLandingPage' + ); + }); + + it('CustomizeMyData should display reset layout confirmation modal on click of reset button', async () => { + await act(async () => { + render(); + }); + + const resetButton = screen.getByTestId('reset-button'); + + await act(async () => userEvent.click(resetButton)); + + expect(screen.getByTestId('reset-layout-modal')).toBeInTheDocument(); + }); + + it('CustomizeMyData should call handlePageDataChange with default layout and close the reset confirmation modal', async () => { + await act(async () => { + render(); + }); + + // handlePageDataChange is called 1 time on mount + expect(mockProps.handlePageDataChange).toHaveBeenCalledTimes(1); + + const resetButton = screen.getByTestId('reset-button'); + + await act(async () => userEvent.click(resetButton)); + + expect(screen.getByTestId('reset-layout-modal')).toBeInTheDocument(); + + const yesButton = screen.getByText('label.yes'); + + await act(async () => userEvent.click(yesButton)); + + expect(mockProps.handlePageDataChange).toHaveBeenCalledTimes(3); + // Check if the handlePageDataChange is passed an object with the default layout + expect(mockProps.handlePageDataChange).toHaveBeenCalledWith( + expect.objectContaining({ + ...mockDocumentData, + data: { + page: { + layout: expect.arrayContaining(mockDefaultLayout), + }, + }, + }) + ); + + expect(screen.queryByTestId('reset-layout-modal')).toBeNull(); + }); + + it('CustomizeMyData should close the reset confirmation modal without calling handlePageDataChange', async () => { + await act(async () => { + render(); + }); + + // handlePageDataChange is called 1 time on mount + expect(mockProps.handlePageDataChange).toHaveBeenCalledTimes(1); + + const resetButton = screen.getByTestId('reset-button'); + + await act(async () => userEvent.click(resetButton)); + + expect(screen.getByTestId('reset-layout-modal')).toBeInTheDocument(); + + const noButton = screen.getByText('label.no'); + + await act(async () => userEvent.click(noButton)); + + // handlePageDataChange is not called again + expect(mockProps.handlePageDataChange).toHaveBeenCalledTimes(1); + + expect(screen.queryByTestId('reset-layout-modal')).toBeNull(); + }); + + it('CustomizeMyData should call onSaveLayout after clicking on save layout button', async () => { + await act(async () => { + render(); + }); + + expect(mockProps.onSaveLayout).toHaveBeenCalledTimes(0); + + const saveButton = screen.getByTestId('save-button'); + + await act(async () => userEvent.click(saveButton)); + + expect(mockProps.onSaveLayout).toHaveBeenCalledTimes(1); + + expect(screen.queryByTestId('reset-layout-modal')).toBeNull(); + }); + + it('CustomizeMyData should display EmptyWidgetPlaceholder', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByText('EmptyWidgetPlaceholder')).toBeInTheDocument(); + }); + + it('CustomizeMyData should display AddWidgetModal after handleOpenAddWidgetModal is called', async () => { + await act(async () => { + render(); + }); + + const addWidgetButton = screen.getByText('handleOpenAddWidgetModal'); + + await act(async () => userEvent.click(addWidgetButton)); + + expect(screen.getByText('AddWidgetModal')).toBeInTheDocument(); + }); + + it('CustomizeMyData should not display AddWidgetModal after handleCloseAddWidgetModal is called', async () => { + await act(async () => { + render(); + }); + + const addWidgetButton = screen.getByText('handleOpenAddWidgetModal'); + + await act(async () => userEvent.click(addWidgetButton)); + + expect(screen.getByText('AddWidgetModal')).toBeInTheDocument(); + + const closeWidgetButton = screen.getByText('handleCloseAddWidgetModal'); + + await act(async () => userEvent.click(closeWidgetButton)); + + expect(screen.queryByText('AddWidgetModal')).toBeNull(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/CustomizeMyData/CustomizeMyData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/CustomizeMyData/CustomizeMyData.tsx index 6e2625b8f10..0ddf4ed409f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/CustomizeMyData/CustomizeMyData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/CustomizeMyData/CustomizeMyData.tsx @@ -13,7 +13,7 @@ import { Button, Col, Modal, Space, Typography } from 'antd'; import { AxiosError } from 'axios'; -import { isEmpty, isNil, uniqBy } from 'lodash'; +import { isEmpty, isNil } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import RGL, { Layout, WidthProvider } from 'react-grid-layout'; import { useTranslation } from 'react-i18next'; @@ -26,12 +26,10 @@ import { import { LandingPageWidgetKeys } from '../../../enums/CustomizablePage.enum'; import { AssetsType } from '../../../enums/entity.enum'; import { Document } from '../../../generated/entity/docStore/document'; -import { Thread } from '../../../generated/entity/feed/thread'; import { EntityReference } from '../../../generated/entity/type'; import { PageType } from '../../../generated/system/ui/page'; import { WidgetConfig } from '../../../pages/CustomizablePage/CustomizablePage.interface'; import '../../../pages/MyDataPage/my-data.less'; -import { getActiveAnnouncement } from '../../../rest/feedsAPI'; import { getUserById } from '../../../rest/userAPI'; import { Transi18next } from '../../../utils/CommonUtils'; import { @@ -39,6 +37,7 @@ import { getLayoutUpdateHandler, getLayoutWithEmptyWidgetPlaceholder, getRemoveWidgetHandler, + getUniqueFilteredLayout, getWidgetFromKey, } from '../../../utils/CustomizableLandingPageUtils'; import customizePageClassBase from '../../../utils/CustomizePageClassBase'; @@ -86,9 +85,6 @@ function CustomizeMyData({ const [followedData, setFollowedData] = useState>(); const [followedDataCount, setFollowedDataCount] = useState(0); const [isLoadingOwnedData, setIsLoadingOwnedData] = useState(false); - const [isAnnouncementLoading, setIsAnnouncementLoading] = - useState(true); - const [announcements, setAnnouncements] = useState([]); const decodedPersonaFQN = useMemo( () => getDecodedFqn(personaFQN), @@ -185,7 +181,6 @@ function CustomizeMyData({ layout.map((widget) => (
{getWidgetFromKey({ - announcements: announcements, followedData: followedData ?? [], followedDataCount: followedDataCount, isLoadingOwnedData: isLoadingOwnedData, @@ -194,53 +189,26 @@ function CustomizeMyData({ handlePlaceholderWidgetKey: handlePlaceholderWidgetKey, handleRemoveWidget: handleRemoveWidget, isEditView: true, - isAnnouncementLoading: isAnnouncementLoading, })}
)), [ layout, - announcements, followedData, followedDataCount, isLoadingOwnedData, handleOpenAddWidgetModal, handlePlaceholderWidgetKey, handleRemoveWidget, - isAnnouncementLoading, ] ); - const fetchAnnouncements = useCallback(async () => { - try { - setIsAnnouncementLoading(true); - const response = await getActiveAnnouncement(); - - setAnnouncements(response.data); - } catch (error) { - showErrorToast(error as AxiosError); - } finally { - setIsAnnouncementLoading(false); - } - }, []); - - useEffect(() => { - fetchAnnouncements(); - }, []); - useEffect(() => { handlePageDataChange({ ...initialPageData, data: { page: { - layout: uniqBy( - layout.filter( - (widget) => - widget.i.startsWith('KnowledgePanel') && - !widget.i.endsWith('.EmptyWidgetPlaceholder') - ), - 'i' - ), + layout: getUniqueFilteredLayout(layout), }, }, }); @@ -256,13 +224,18 @@ function CustomizeMyData({ }, []); const handleReset = useCallback(() => { - const newMainPanelLayout = customizePageClassBase.defaultLayout; + // Get default layout with the empty widget added at the end + const newMainPanelLayout = getLayoutWithEmptyWidgetPlaceholder( + customizePageClassBase.defaultLayout, + 2, + 4 + ); setLayout(newMainPanelLayout); handlePageDataChange({ ...initialPageData, data: { page: { - layout: uniqBy(newMainPanelLayout, 'i'), + layout: getUniqueFilteredLayout(newMainPanelLayout), }, }, }); @@ -280,9 +253,13 @@ function CustomizeMyData({ header={
- +
- - - @@ -348,6 +335,7 @@ function CustomizeMyData({ { + it('EmptyWidgetPlaceholder should render properly', async () => { + await act(async () => { + render( + + ); + }); + + expect( + screen.getByTestId(LandingPageWidgetKeys.ACTIVITY_FEED) + ).toBeInTheDocument(); + expect(screen.getByTestId('drag-widget-button')).toBeInTheDocument(); + expect(screen.getByTestId('remove-widget-button')).toBeInTheDocument(); + expect(screen.getByTestId('no-data-image')).toBeInTheDocument(); + expect( + screen.getByText('message.adding-new-entity-is-easy-just-give-it-a-spin') + ).toBeInTheDocument(); + expect(screen.getByTestId('add-widget-button')).toBeInTheDocument(); + }); + + it('EmptyWidgetPlaceholder should display drag and remove buttons when isEditable is not passed', async () => { + await act(async () => { + render(); + }); + + expect(screen.getByTestId('drag-widget-button')).toBeInTheDocument(); + expect(screen.getByTestId('remove-widget-button')).toBeInTheDocument(); + }); + + it('EmptyWidgetPlaceholder should not display drag and remove buttons when isEditable is false', async () => { + await act(async () => { + render(); + }); + + expect(screen.queryByTestId('drag-widget-button')).toBeNull(); + expect(screen.queryByTestId('remove-widget-button')).toBeNull(); + }); + + it('EmptyWidgetPlaceholder should call handleAddClick after clicking on add widget button', async () => { + await act(async () => { + render(); + }); + + expect(mockProps.handleOpenAddWidgetModal).toHaveBeenCalledTimes(0); + expect(mockProps.handlePlaceholderWidgetKey).toHaveBeenCalledTimes(0); + + const addButton = screen.getByTestId('add-widget-button'); + + await act(async () => userEvent.click(addButton)); + + expect(mockProps.handleOpenAddWidgetModal).toHaveBeenCalledTimes(1); + expect(mockProps.handlePlaceholderWidgetKey).toHaveBeenCalledTimes(1); + expect(mockProps.handlePlaceholderWidgetKey).toHaveBeenCalledWith( + mockProps.widgetKey + ); + }); + + it('EmptyWidgetPlaceholder should call handleRemoveWidget when clicked on remove widget button', async () => { + await act(async () => { + render(); + }); + + expect(mockProps.handleRemoveWidget).toHaveBeenCalledTimes(0); + + const removeButton = screen.getByTestId('remove-widget-button'); + + await act(async () => userEvent.click(removeButton)); + + expect(mockProps.handleRemoveWidget).toHaveBeenCalledTimes(1); + expect(mockProps.handleRemoveWidget).toHaveBeenCalledWith( + mockProps.widgetKey + ); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/EmptyWidgetPlaceholder/EmptyWidgetPlaceholder.tsx b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/EmptyWidgetPlaceholder/EmptyWidgetPlaceholder.tsx index 4c3dc4cc08e..7130c453c8c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/EmptyWidgetPlaceholder/EmptyWidgetPlaceholder.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/CustomizableComponents/EmptyWidgetPlaceholder/EmptyWidgetPlaceholder.tsx @@ -42,7 +42,10 @@ function EmptyWidgetPlaceholder({ }, []); return ( - + {isEditable && ( @@ -50,11 +53,16 @@ function EmptyWidgetPlaceholder({ - + @@ -80,7 +88,7 @@ function EmptyWidgetPlaceholder({