#11374: Removed the announcement knowledge panel customisability (#14196)

* Removed the announcement knowledge panel
Announcements widget will only show on landing page if  announcements are present

* renamed the MyDataPageV1 to  MyDataPage

* added unit tests for the MyDataPage component

* Added unit tests for customizablePage and MyDataPage

* removed the API call to fetch announcements in customizeMyData component

* added unit tests for CustomizeMyData component

* added unit tests for EmptyWidgetPlaceholder component

* Added unit tests for AddWidgetModal components

* updated the data test ids for widgets

* updated the data test id for KPI widget

* added data-testids related to customize page flow

* Added tests for persona CRUD operations

* localization change for other languages

* updated data test ids related to customize landing page

* updated the persona flow spec

* added cypress tests for customizable landing page flow

* fixed unit tests

* updated the my data page logic to add announcement widget

* fixed empty widget placeholder not showing after resetting the layout

* Minor: Fix the DocStoreResourceTest

* remove the border for announcement widget

---------

Co-authored-by: Sriharsha Chintalapani <harsha@getcollate.io>
This commit is contained in:
Aniket Katkar 2024-01-05 11:30:00 +05:30 committed by GitHub
parent ac39e3e262
commit 5a0880ccaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 2559 additions and 186 deletions

View File

@ -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"]
}
}

View File

@ -148,7 +148,7 @@ public class DocStoreResourceTest extends EntityResourceTest<Document, CreateDoc
queryParams.put(
"fqnPrefix", FullyQualifiedName.build(knowledgePanel.getEntityType().toString()));
ResultList<Document> panelList = listEntities(queryParams, ADMIN_AUTH_HEADERS);
assertEquals(panelDocs.size() + 7, panelList.getPaging().getTotal());
assertEquals(panelDocs.size() + 6, panelList.getPaging().getTotal());
// docs
List<Document> pageDocs = new ArrayList<>();

View File

@ -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
/// <reference types="cypress" />
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'
);
}
};

View File

@ -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.`,
};

View File

@ -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

View File

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

View File

@ -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
/// <reference types="cypress" />
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();
});
});

View File

@ -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
/// <reference types="cypress" />
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!`);
});
});

View File

@ -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 (
<Switch>
<Route exact component={MyDataPageV1} path={ROUTES.MY_DATA} />
<Route exact component={MyDataPage} path={ROUTES.MY_DATA} />
<Route exact component={TourPageComponent} path={ROUTES.TOUR} />
<Route exact component={ExplorePageV1} path={ROUTES.EXPLORE} />
<Route component={ExplorePageV1} path={ROUTES.EXPLORE_WITH_TAB} />

View File

@ -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';

View File

@ -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;
}

View File

@ -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(() => <div>Loader</div>)
);
jest.mock('../../common/ErrorWithPlaceholder/ErrorPlaceHolder', () =>
jest.fn().mockImplementation(() => <div>ErrorPlaceHolder</div>)
);
jest.mock('./AddWidgetTabContent', () =>
jest.fn().mockImplementation(({ getAddWidgetHandler }) => (
<div>
AddWidgetTabContent
<div onClick={getAddWidgetHandler(mockWidgetsData.data[0], 3)}>
getAddWidgetHandler
</div>
</div>
))
);
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(<AddWidgetModal {...mockProps} open={false} />);
});
expect(screen.queryByTestId('add-widget-modal')).toBeNull();
});
it('AddWidgetModal should display all widgets tab from the widgets list', async () => {
await act(async () => {
render(<AddWidgetModal {...mockProps} />);
});
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(
<AddWidgetModal
{...mockProps}
addedWidgetsList={[
'KnowledgePanel.ActivityFeed',
'KnowledgePanel.Following',
]}
/>
);
});
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(<AddWidgetModal {...mockProps} />);
});
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(<AddWidgetModal {...mockProps} />);
});
expect(screen.getByText('ErrorPlaceHolder')).toBeInTheDocument();
});
it('AddWidgetModal should', async () => {
await act(async () => {
render(<AddWidgetModal {...mockProps} />);
});
});
});

View File

@ -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<AddWidgetModalProps>) {
const { t } = useTranslation();
const [widgetsList, setWidgetsList] = useState<Array<Document>>();
const [loading, setLoading] = useState<boolean>(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<WidgetSizeInfo> =
widget.data.gridSizes.map((size: WidgetWidths) => ({
label: getWidgetWidthLabelFromKey(toString(size)),
label: (
<span data-testid={`${size}-size-selector`}>
{getWidgetWidthLabelFromKey(toString(size))}
</span>
),
value: WidgetWidths[size],
}));
return {
label: (
<Space>
<Space data-testid={`${widget.name}-widget-tab-label`}>
<span>{widget.name}</span>
{addedWidgetsList.some((w) =>
w.startsWith(widget.fullyQualifiedName)
) && (
<CheckOutlined
className="m-l-xs"
data-testid={`${widget.name}-check-icon`}
style={{ color: '#4CAF50' }}
/>
)}
@ -101,24 +111,42 @@ function AddWidgetModal({
fetchKnowledgePanels();
}, []);
const widgetsInfo = useMemo(() => {
if (loading) {
return <Loader />;
}
if (isEmpty(widgetsList)) {
return (
<ErrorPlaceHolder
className="h-min-480"
data-testid="no-widgets-placeholder"
type={ERROR_PLACEHOLDER_TYPE.CUSTOM}>
{t('message.no-widgets-to-add')}
</ErrorPlaceHolder>
);
}
return (
<Tabs
data-testid="widget-info-tabs"
items={tabItems}
tabPosition="left"
/>
);
}, [loading, widgetsList, tabItems]);
return (
<Modal
centered
className="add-widget-modal"
data-testid="add-widget-modal"
footer={null}
open={open}
title={t('label.add-new-entity', { entity: t('label.widget') })}
width={750}
onCancel={handleCloseAddWidgetModal}>
{isEmpty(widgetsList) ? (
<ErrorPlaceHolder
className="h-min-480"
type={ERROR_PLACEHOLDER_TYPE.CUSTOM}>
{t('message.no-widgets-to-add')}
</ErrorPlaceHolder>
) : (
<Tabs items={tabItems} tabPosition="left" />
)}
{widgetsInfo}
</Modal>
);
}

View File

@ -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(<AddWidgetTabContent {...mockProps} />);
});
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(<AddWidgetTabContent {...mockProps} />);
});
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(<AddWidgetTabContent {...mockProps} />);
});
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(<AddWidgetTabContent {...mockProps} maxGridSizeSupport={0} />);
});
expect(screen.getByTestId('add-widget-button')).toBeDisabled();
});
});

View File

@ -63,6 +63,7 @@ function AddWidgetTabContent({
<Space>
<Typography.Text>{`${t('label.size')}:`}</Typography.Text>
<Radio.Group
data-testid="size-selector-button"
defaultValue={selectedWidgetSize}
optionType="button"
options={widgetSizeOptions}
@ -74,8 +75,15 @@ function AddWidgetTabContent({
<Row className="h-min-480" justify="center">
<Col>
<Space align="center" direction="vertical">
<Image className="p-y-md" preview={false} src={widgetImage} />
<Typography.Paragraph className="d-block text-center">
<Image
className="p-y-md"
data-testid="widget-image"
preview={false}
src={widgetImage}
/>
<Typography.Paragraph
className="d-block text-center"
data-testid="widget-description">
{widget.description}
</Typography.Paragraph>
<Tooltip
@ -84,7 +92,7 @@ function AddWidgetTabContent({
<Button
ghost
className="p-x-lg m-t-md"
data-testid="add-widget-placeholder-button"
data-testid="add-widget-button"
disabled={!widgetAddable}
icon={<PlusOutlined />}
type="primary"

View File

@ -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 }) => (
<div data-testid="activity-feed-provider">{children}</div>
));
}
);
jest.mock('../AddWidgetModal/AddWidgetModal', () => {
return jest.fn().mockImplementation(({ handleCloseAddWidgetModal }) => (
<div>
AddWidgetModal
<div onClick={handleCloseAddWidgetModal}>handleCloseAddWidgetModal</div>
</div>
));
});
jest.mock('../EmptyWidgetPlaceholder/EmptyWidgetPlaceholder', () => {
return jest.fn().mockImplementation(({ handleOpenAddWidgetModal }) => (
<div>
EmptyWidgetPlaceholder
<div onClick={handleOpenAddWidgetModal}>handleOpenAddWidgetModal</div>
</div>
));
});
jest.mock('../../../utils/CustomizePageClassBase', () => {
return mockCustomizePageClassBase;
});
jest.mock('../../PageLayoutV1/PageLayoutV1', () => {
return jest.fn().mockImplementation(({ children, header }) => (
<div data-testid="page-layout-v1">
<div data-testid="page-header">{header}</div>
{children}
</div>
));
});
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(() => <div>Link</div>),
}));
jest.mock('react-grid-layout', () => ({
WidthProvider: jest
.fn()
.mockImplementation(() =>
jest
.fn()
.mockImplementation(({ children }) => (
<div data-testid="react-grid-layout">{children}</div>
))
),
__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(<CustomizeMyData {...mockProps} />);
});
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(<CustomizeMyData {...mockProps} />);
});
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(<CustomizeMyData {...mockProps} />);
});
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(<CustomizeMyData {...mockProps} />);
});
// 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<WidgetConfig>(mockDefaultLayout),
},
},
})
);
expect(screen.queryByTestId('reset-layout-modal')).toBeNull();
});
it('CustomizeMyData should close the reset confirmation modal without calling handlePageDataChange', async () => {
await act(async () => {
render(<CustomizeMyData {...mockProps} />);
});
// 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(<CustomizeMyData {...mockProps} />);
});
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(<CustomizeMyData {...mockProps} />);
});
expect(screen.getByText('EmptyWidgetPlaceholder')).toBeInTheDocument();
});
it('CustomizeMyData should display AddWidgetModal after handleOpenAddWidgetModal is called', async () => {
await act(async () => {
render(<CustomizeMyData {...mockProps} />);
});
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(<CustomizeMyData {...mockProps} />);
});
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();
});
});

View File

@ -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<Array<EntityReference>>();
const [followedDataCount, setFollowedDataCount] = useState(0);
const [isLoadingOwnedData, setIsLoadingOwnedData] = useState<boolean>(false);
const [isAnnouncementLoading, setIsAnnouncementLoading] =
useState<boolean>(true);
const [announcements, setAnnouncements] = useState<Thread[]>([]);
const decodedPersonaFQN = useMemo(
() => getDecodedFqn(personaFQN),
@ -185,7 +181,6 @@ function CustomizeMyData({
layout.map((widget) => (
<div data-grid={widget} id={widget.i} key={widget.i}>
{getWidgetFromKey({
announcements: announcements,
followedData: followedData ?? [],
followedDataCount: followedDataCount,
isLoadingOwnedData: isLoadingOwnedData,
@ -194,53 +189,26 @@ function CustomizeMyData({
handlePlaceholderWidgetKey: handlePlaceholderWidgetKey,
handleRemoveWidget: handleRemoveWidget,
isEditView: true,
isAnnouncementLoading: isAnnouncementLoading,
})}
</div>
)),
[
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={
<Col
className="bg-white d-flex justify-between border-bottom p-sm"
data-testid="customize-landing-page-header"
span={24}>
<div className="d-flex gap-2 items-center">
<Typography.Title className="m-0" level={5}>
<Typography.Title
className="m-0"
data-testid="customize-page-title"
level={5}>
<Transi18next
i18nKey="message.customize-landing-page-header"
renderElement={
@ -300,13 +277,23 @@ function CustomizeMyData({
</Typography.Title>
</div>
<Space>
<Button size="small" onClick={handleCancel}>
<Button
data-testid="cancel-button"
size="small"
onClick={handleCancel}>
{t('label.cancel')}
</Button>
<Button size="small" onClick={handleOpenResetModal}>
<Button
data-testid="reset-button"
size="small"
onClick={handleOpenResetModal}>
{t('label.reset')}
</Button>
<Button size="small" type="primary" onClick={onSaveLayout}>
<Button
data-testid="save-button"
size="small"
type="primary"
onClick={onSaveLayout}>
{t('label.save')}
</Button>
</Space>
@ -348,6 +335,7 @@ function CustomizeMyData({
<Modal
centered
cancelText={t('label.no')}
data-testid="reset-layout-modal"
okText={t('label.yes')}
open={isResetModalOpen}
title={t('label.reset-default-layout')}

View File

@ -0,0 +1,109 @@
/*
* 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 { SIZE } from '../../../enums/common.enum';
import { LandingPageWidgetKeys } from '../../../enums/CustomizablePage.enum';
import EmptyWidgetPlaceholder from './EmptyWidgetPlaceholder';
import { EmptyWidgetPlaceholderProps } from './EmptyWidgetPlaceholder.interface';
const mockProps: EmptyWidgetPlaceholderProps = {
iconHeight: SIZE.MEDIUM,
iconWidth: SIZE.MEDIUM,
widgetKey: LandingPageWidgetKeys.ACTIVITY_FEED,
handleOpenAddWidgetModal: jest.fn(),
handlePlaceholderWidgetKey: jest.fn(),
handleRemoveWidget: jest.fn(),
isEditable: true,
};
describe('EmptyWidgetPlaceholder component', () => {
it('EmptyWidgetPlaceholder should render properly', async () => {
await act(async () => {
render(
<EmptyWidgetPlaceholder
{...mockProps}
iconHeight={undefined}
iconWidth={undefined}
/>
);
});
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(<EmptyWidgetPlaceholder {...mockProps} isEditable={undefined} />);
});
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(<EmptyWidgetPlaceholder {...mockProps} isEditable={false} />);
});
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(<EmptyWidgetPlaceholder {...mockProps} />);
});
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(<EmptyWidgetPlaceholder {...mockProps} />);
});
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
);
});
});

View File

@ -42,7 +42,10 @@ function EmptyWidgetPlaceholder({
}, []);
return (
<Card bodyStyle={{ height: '100%' }} className="empty-widget-placeholder">
<Card
bodyStyle={{ height: '100%' }}
className="empty-widget-placeholder"
data-testid={widgetKey}>
<Row className="h-full">
{isEditable && (
<Col span={24}>
@ -50,11 +53,16 @@ function EmptyWidgetPlaceholder({
<Col>
<DragOutlined
className="drag-widget-icon cursor-pointer"
data-testid="drag-widget-button"
size={14}
/>
</Col>
<Col>
<CloseOutlined size={14} onClick={handleCloseClick} />
<CloseOutlined
data-testid="remove-widget-button"
size={14}
onClick={handleCloseClick}
/>
</Col>
</Row>
</Col>
@ -80,7 +88,7 @@ function EmptyWidgetPlaceholder({
<Button
ghost
className="add-button"
data-testid="add-widget-placeholder-button"
data-testid="add-widget-button"
icon={<PlusOutlined />}
type="primary"
onClick={handleAddClick}>

View File

@ -16,7 +16,7 @@ import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ReactComponent as ExclamationIcon } from '../../../assets/svg/ic-exclamation-circle.svg';
import { ClientErrors } from '../../../enums/axios.enum';
import { ClientErrors } from '../../../enums/Axios.enum';
import { EntityType } from '../../../enums/entity.enum';
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
import { EntityReference } from '../../../generated/entity/type';

View File

@ -216,7 +216,7 @@ const KPIWidget = ({
return (
<Card
className="kpi-widget-card h-full"
data-testid="kpi-card"
data-testid="kpi-widget"
id="kpi-charts"
loading={isKPIListLoading || isLoading}>
{isEditView && (
@ -225,9 +225,14 @@ const KPIWidget = ({
<Space align="center">
<DragOutlined
className="drag-widget-icon cursor-pointer"
data-testid="drag-widget-button"
size={14}
/>
<CloseOutlined size={14} onClick={handleCloseClick} />
<CloseOutlined
data-testid="remove-widget-button"
size={14}
onClick={handleCloseClick}
/>
</Space>
</Col>
</Row>

View File

@ -91,7 +91,10 @@ const MyDataWidgetInternal = ({
}, [currentUser]);
return (
<Card className="my-data-widget-container card-widget" loading={isLoading}>
<Card
className="my-data-widget-container card-widget"
data-testid="my-data-widget"
loading={isLoading}>
<Row>
<Col span={24}>
<div className="d-flex justify-between m-b-xs">
@ -115,9 +118,14 @@ const MyDataWidgetInternal = ({
<>
<DragOutlined
className="drag-widget-icon cursor-pointer"
data-testid="drag-widget-button"
size={14}
/>
<CloseOutlined size={14} onClick={handleCloseClick} />
<CloseOutlined
data-testid="remove-widget-button"
size={14}
onClick={handleCloseClick}
/>
</>
)}
</Space>

View File

@ -82,8 +82,8 @@ jest.mock(
);
describe('MyDataWidget component', () => {
it('should fetch data', () => {
act(() => {
it('should fetch data', async () => {
await act(async () => {
render(<MyDataWidget widgetKey="widgetKey" />, { wrapper: MemoryRouter });
});
@ -98,8 +98,8 @@ describe('MyDataWidget component', () => {
);
});
it.skip('should render header', () => {
act(() => {
it.skip('should render header', async () => {
await act(async () => {
render(
<MemoryRouter>
<MyDataWidget widgetKey="widgetKey" />
@ -110,8 +110,8 @@ describe('MyDataWidget component', () => {
expect(screen.getByText('label.my-data')).toBeInTheDocument();
});
it('should not render view all for 0 length data', () => {
act(() => {
it('should not render view all for 0 length data', async () => {
await act(async () => {
render(
<MemoryRouter>
<MyDataWidget widgetKey="widgetKey" />
@ -126,7 +126,7 @@ describe('MyDataWidget component', () => {
(searchData as jest.Mock).mockImplementationOnce(() =>
Promise.resolve(mockSearchAPIResponse)
);
act(() => {
await act(async () => {
render(
<MemoryRouter>
<MyDataWidget widgetKey="widgetKey" />
@ -139,7 +139,7 @@ describe('MyDataWidget component', () => {
it('should render table names', async () => {
(searchData as jest.Mock).mockResolvedValueOnce(mockSearchAPIResponse);
act(() => {
await act(async () => {
render(
<MemoryRouter>
<MyDataWidget widgetKey="widgetKey" />

View File

@ -10,10 +10,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CloseOutlined, DragOutlined } from '@ant-design/icons';
import { Alert, Card, Col, Row, Space, Typography } from 'antd';
import { isEmpty, isUndefined } from 'lodash';
import React, { useCallback, useMemo } from 'react';
import { Alert, Col, Row, Typography } from 'antd';
import { isEmpty } from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as AnnouncementIcon } from '../../../assets/svg/announcements-v1.svg';
import { ReactComponent as AnnouncementsEmptyIcon } from '../../../assets/svg/announcment-no-data-placeholder.svg';
@ -28,22 +27,15 @@ import './announcements-widget.less';
export interface AnnouncementsWidgetProps extends WidgetCommonProps {
isAnnouncementLoading?: boolean;
announcements: Thread[];
announcements?: Thread[];
}
function AnnouncementsWidget({
announcements,
isEditView,
handleRemoveWidget,
widgetKey,
announcements = [],
isAnnouncementLoading = false,
}: Readonly<AnnouncementsWidgetProps>) {
const { t } = useTranslation();
const handleCloseClick = useCallback(() => {
!isUndefined(handleRemoveWidget) && handleRemoveWidget(widgetKey);
}, [widgetKey]);
const announcement = useMemo(() => {
if (isAnnouncementLoading) {
return <Loader size="small" />;
@ -115,27 +107,16 @@ function AnnouncementsWidget({
}, [isAnnouncementLoading, announcements]);
return (
<Card className="announcement-container card-widget h-full">
<div className="announcement-container card-widget h-full">
<Row justify="space-between">
<Col>
<Typography.Paragraph className="font-medium m-b-sm">
{t('label.recent-announcement-plural')}
</Typography.Paragraph>
</Col>
{isEditView && (
<Col>
<Space>
<DragOutlined
className="drag-widget-icon cursor-pointer"
size={14}
/>
<CloseOutlined size={14} onClick={handleCloseClick} />
</Space>
</Col>
)}
</Row>
{announcement}
</Card>
</div>
);
}

View File

@ -50,7 +50,7 @@ function FollowingWidget({
return (
<Card
className="following-widget-container card-widget h-full"
data-testid="following-data-container">
data-testid="following-widget">
<EntityListWithV1
entityList={followedData}
headerText={
@ -72,9 +72,14 @@ function FollowingWidget({
<>
<DragOutlined
className="drag-widget-icon cursor-pointer"
data-testid="drag-widget-button"
size={14}
/>
<CloseOutlined size={14} onClick={handleCloseClick} />
<CloseOutlined
data-testid="remove-widget-button"
size={14}
onClick={handleCloseClick}
/>
</>
)}
</Space>

View File

@ -17,7 +17,7 @@
flex-direction: column;
}
.announcement-container-list {
overflow-y: auto;
overflow-y: scroll;
overflow-x: hidden;
max-height: 90%;
.feed-card-body {

View File

@ -18,7 +18,7 @@ import { HeaderProps } from './PageHeader.interface';
const PageHeader = ({ data: { header, subHeader } }: HeaderProps) => {
return (
<div className="page-header-container">
<div className="page-header-container" data-testid="page-header-container">
<Typography.Title className="heading" data-testid="heading" level={5}>
{header}
</Typography.Title>

View File

@ -138,7 +138,7 @@ export const AddEditPersonaForm = ({
hasPermission: true,
children: (
<Button
data-testid="add-reviewers"
data-testid="add-users"
icon={<PlusOutlined style={{ color: 'white', fontSize: '12px' }} />}
size="small"
type="primary"
@ -160,6 +160,7 @@ export const AddEditPersonaForm = ({
closable={false}
closeIcon={null}
confirmLoading={isSaving}
data-testid="add-edit-persona-modal"
okText={isEditMode ? t('label.update') : t('label.create')}
title={isEmpty(persona) ? 'Add Persona' : 'Edit Persona'}
width={750}
@ -178,7 +179,7 @@ export const AddEditPersonaForm = ({
<Space
wrap
className="m--t-md"
data-testid="reviewers-container"
data-testid="users-container"
size={[8, 8]}>
{usersList.map((d) => (
<UserTag

View File

@ -37,6 +37,7 @@ export const PersonaDetailsCard = ({ persona }: PersonaDetailsCardProps) => {
<Card
bodyStyle={{ height: '100%' }}
className="h-full cursor-pointer"
data-testid="persona-details-card"
onClick={handleCardClick}>
<Space className="justify-between h-full" direction="vertical">
<Card.Meta

View File

@ -112,7 +112,7 @@ const TotalDataAssetsWidget = ({
return (
<Card
className="total-data-insight-card"
data-testid="entity-summary-card"
data-testid="total-assets-widget"
id={DataInsightChartType.TotalEntitiesByType}
loading={isLoading}>
{isEditView && (
@ -120,11 +120,16 @@ const TotalDataAssetsWidget = ({
<Col>
<DragOutlined
className="drag-widget-icon cursor-pointer"
data-testid="drag-widget-button"
size={14}
/>
</Col>
<Col>
<CloseOutlined size={14} onClick={handleCloseClick} />
<CloseOutlined
data-testid="remove-widget-button"
size={14}
onClick={handleCloseClick}
/>
</Col>
</Row>
)}

View File

@ -10,10 +10,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Icon from '@ant-design/icons';
import React, { useEffect, useMemo, useState } from 'react';
import { Button, Modal } from 'antd';
import { isNil } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as CloseIcon } from '../../../assets/svg/ic-delete.svg';
import { ReactComponent as IconRemove } from '../../../assets/svg/ic-remove.svg';
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
import { User } from '../../../generated/entity/teams/user';
import { EntityReference } from '../../../generated/entity/type';
@ -32,9 +33,29 @@ export const UsersTab = ({ users, onRemoveUser }: UsersTabProps) => {
const [additionalUsersDetails, setAdditionalUsersDetails] = useState<User[]>(
[]
);
const [removeUserDetails, setRemoveUserDetails] =
useState<{ state: boolean; user: User }>();
const { t } = useTranslation();
const handleRemoveButtonClick = useCallback((user: User) => {
setRemoveUserDetails({
state: true,
user,
});
}, []);
const handleRemoveCancel = useCallback(() => {
setRemoveUserDetails(undefined);
}, []);
const handleRemoveConfirm = useCallback(() => {
if (!isNil(removeUserDetails) && !isNil(onRemoveUser)) {
onRemoveUser(removeUserDetails.user.id);
}
handleRemoveCancel();
}, [removeUserDetails, handleRemoveCancel]);
const fetchUsersAdditionalDetails = async () => {
try {
setIsDetailsLoading(true);
@ -63,19 +84,23 @@ export const UsersTab = ({ users, onRemoveUser }: UsersTabProps) => {
title: t('label.action-plural'),
dataIndex: 'id',
key: 'id',
render: (id: string) => {
width: 90,
render: (_: string, record: User) => {
return (
onRemoveUser && (
<Icon
component={CloseIcon}
title={t('label.remove')}
onClick={() => onRemoveUser(id)}
<Button
data-testid="remove-user-btn"
icon={
<IconRemove height={16} name={t('label.remove')} width={16} />
}
type="text"
onClick={() => handleRemoveButtonClick(record)}
/>
)
);
},
};
}, []);
}, [onRemoveUser]);
const columns = useMemo(
() => [...commonUserDetailColumns(isDetailsLoading), actionColumn],
@ -83,26 +108,44 @@ export const UsersTab = ({ users, onRemoveUser }: UsersTabProps) => {
);
return (
<Table
bordered
columns={columns}
dataSource={
isDetailsLoading ? (users as unknown as User[]) : additionalUsersDetails
}
loading={isDetailsLoading}
locale={{
emptyText: (
<ErrorPlaceHolder
permission
className="p-y-md"
heading={t('label.user')}
type={ERROR_PLACEHOLDER_TYPE.ASSIGN}
/>
),
}}
pagination={false}
rowKey="fullyQualifiedName"
size="small"
/>
<>
<Table
bordered
columns={columns}
dataSource={
isDetailsLoading
? (users as unknown as User[])
: additionalUsersDetails
}
loading={isDetailsLoading}
locale={{
emptyText: (
<ErrorPlaceHolder
permission
className="p-y-md"
heading={t('label.user')}
type={ERROR_PLACEHOLDER_TYPE.ASSIGN}
/>
),
}}
pagination={false}
rowKey="fullyQualifiedName"
size="small"
/>
<Modal
cancelText={t('label.cancel')}
data-testid="remove-confirmation-modal"
okText={t('label.confirm')}
open={Boolean(removeUserDetails?.state)}
title={t('label.removing-user')}
onCancel={handleRemoveCancel}
onOk={handleRemoveConfirm}>
{t('message.are-you-sure-want-to-text', {
text: t('label.remove-entity-lowercase', {
entity: removeUserDetails?.user.name,
}),
})}
</Modal>
</>
);
};

View File

@ -215,9 +215,14 @@ const FeedsWidget = ({
<>
<DragOutlined
className="drag-widget-icon cursor-pointer"
data-testid="drag-widget-button"
size={14}
/>
<CloseOutlined size={14} onClick={handleCloseClick} />
<CloseOutlined
data-testid="remove-widget-button"
size={14}
onClick={handleCloseClick}
/>
</>
)}
</Space>

View File

@ -72,7 +72,7 @@ const RecentlyViewed = ({
return (
<Card
className="recently-viewed-widget-container card-widget"
data-testid="recently-viewed-container">
data-testid="recently-viewed-widget">
<EntityListSkeleton
dataLength={data.length !== 0 ? data.length : 5}
loading={Boolean(isLoading)}>
@ -88,9 +88,14 @@ const RecentlyViewed = ({
<Space>
<DragOutlined
className="drag-widget-icon cursor-pointer"
data-testid="drag-widget-button"
size={14}
/>
<CloseOutlined size={14} onClick={handleCloseClick} />
<CloseOutlined
data-testid="remove-widget-button"
size={14}
onClick={handleCloseClick}
/>
</Space>
</Col>
)}

View File

@ -827,6 +827,7 @@
"relevance": "Relevanz",
"remove": "Entfernen",
"remove-entity": "{{entity}} entfernen",
"remove-entity-lowercase": "remove {{entity}}",
"remove-lowercase": "remove",
"removed": "Entfernt",
"removing-user": "Benutzer entfernen",

View File

@ -827,6 +827,7 @@
"relevance": "Relevance",
"remove": "Remove",
"remove-entity": "Remove {{entity}}",
"remove-entity-lowercase": "remove {{entity}}",
"remove-lowercase": "remove",
"removed": "Removed",
"removing-user": "Removing User",

View File

@ -827,6 +827,7 @@
"relevance": "Relevancia",
"remove": "Eliminar",
"remove-entity": "Eliminar {{entity}}",
"remove-entity-lowercase": "remove {{entity}}",
"remove-lowercase": "remove",
"removed": "Eliminado",
"removing-user": "Eliminando usuario",

View File

@ -827,6 +827,7 @@
"relevance": "Pertinence",
"remove": "Retirer",
"remove-entity": "Retirer un·e {{entity}}",
"remove-entity-lowercase": "remove {{entity}}",
"remove-lowercase": "remove",
"removed": "Retiré",
"removing-user": "Retirer un Utilisateur",

View File

@ -827,6 +827,7 @@
"relevance": "Relevance",
"remove": "除外",
"remove-entity": "{{entity}}を除外",
"remove-entity-lowercase": "remove {{entity}}",
"remove-lowercase": "remove",
"removed": "除外",
"removing-user": "ユーザを除外する",

View File

@ -827,6 +827,7 @@
"relevance": "Relevância",
"remove": "Remover",
"remove-entity": "Remover {{entity}}",
"remove-entity-lowercase": "remove {{entity}}",
"remove-lowercase": "remover",
"removed": "Removido",
"removing-user": "Removendo Usuário",

View File

@ -827,6 +827,7 @@
"relevance": "Актуальность",
"remove": "Удалить",
"remove-entity": "Удалить {{entity}}",
"remove-entity-lowercase": "remove {{entity}}",
"remove-lowercase": "remove",
"removed": "Удаленный",
"removing-user": "Удаление пользователя",

View File

@ -827,6 +827,7 @@
"relevance": "相关性",
"remove": "删除",
"remove-entity": "删除{{entity}}",
"remove-entity-lowercase": "remove {{entity}}",
"remove-lowercase": "remove",
"removed": "已删除",
"removing-user": "正在删除用户",

View File

@ -0,0 +1,101 @@
/*
* 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 { Document } from '../generated/entity/docStore/document';
import { Paging } from '../generated/type/paging';
export const mockWidgetsData: { data: Document[]; paging: Paging } = {
data: [
{
id: '04fac073-d4a0-42b9-8be1-bf92b45c060b',
name: 'ActivityFeed',
displayName: 'Activity Feed',
fullyQualifiedName: 'KnowledgePanel.ActivityFeed',
description:
'Activity Feed KnowledgePanel shows Activity Feed,Mentions and Tasks that are assigned to User.',
entityType: 'KnowledgePanel',
data: {
gridSizes: ['large'],
},
updatedBy: 'admin',
},
{
id: '5bf549a2-adc7-48be-8a61-17c08aeeaa1b',
name: 'Following',
displayName: 'Following',
fullyQualifiedName: 'KnowledgePanel.Following',
description:
'Following KnowledgePanel shows all the Assets that the User is Following.',
entityType: 'KnowledgePanel',
data: {
gridSizes: ['small'],
},
updatedBy: 'admin',
},
{
id: 'f087d02c-c9ac-4986-800a-36da5074cc98',
name: 'KPI',
displayName: 'KPI',
fullyQualifiedName: 'KnowledgePanel.KPI',
description:
"KPI KnowledgePanel shows the Organization's KPIs on description, owner coverage.",
entityType: 'KnowledgePanel',
data: {
gridSizes: ['small', 'medium'],
},
updatedBy: 'admin',
},
{
id: '3539996d-9980-4d3b-914b-0d3a2f18e09d',
name: 'MyData',
displayName: 'MyData',
fullyQualifiedName: 'KnowledgePanel.MyData',
description:
"MyData KnowledgePanel shows the list of Assets that is owned by User or User's Team.",
entityType: 'KnowledgePanel',
data: {
gridSizes: ['small'],
},
updatedBy: 'admin',
},
{
id: 'd0f5f235-11bf-44d8-b215-0aa8def97d07',
name: 'RecentlyViewed',
displayName: 'Recently Viewed',
fullyQualifiedName: 'KnowledgePanel.RecentlyViewed',
description:
'Recently Viewed KnowledgePanel shows list of Data Assets that User visited.',
entityType: 'KnowledgePanel',
data: {
gridSizes: ['small'],
},
updatedBy: 'admin',
},
{
id: 'bc0d4328-1ef3-4c33-9c79-1c618b18ea92',
name: 'TotalAssets',
displayName: 'Total Assets',
fullyQualifiedName: 'KnowledgePanel.TotalAssets',
description:
'Total Assets KnowledgePanel shows Data Asset growth across the organization.',
entityType: 'KnowledgePanel',
data: {
gridSizes: ['medium', 'large'],
},
updatedBy: 'admin',
},
],
paging: {
total: 6,
},
};

View File

@ -0,0 +1,33 @@
/*
* 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 { Document } from '../generated/entity/docStore/document';
export const mockWidget: Document = {
id: '5bf549a2-adc7-48be-8a61-17c08aeeaa1b',
name: 'Following',
displayName: 'Following',
fullyQualifiedName: 'KnowledgePanel.Following',
description:
'Following KnowledgePanel shows all the Assets that the User is Following.',
entityType: 'KnowledgePanel',
data: {
gridSizes: ['small'],
},
updatedBy: 'admin',
};
export const mockWidgetSizes = [
{ label: 'Small', value: 1 },
{ label: 'Medium', value: 2 },
];

View File

@ -0,0 +1,155 @@
/*
* 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 { LandingPageWidgetKeys } from '../enums/CustomizablePage.enum';
import { Document } from '../generated/entity/docStore/document';
import { Persona } from '../generated/entity/teams/persona';
import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface';
export const mockPersonaName = 'testPersona';
export const mockPersonaDetails: Persona = {
id: 'persona-123',
name: mockPersonaName,
};
const mockDefaultLayout: Array<WidgetConfig> = [
{
h: 6,
i: LandingPageWidgetKeys.ACTIVITY_FEED,
w: 3,
x: 0,
y: 0,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.MY_DATA,
w: 1,
x: 0,
y: 6,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.KPI,
w: 2,
x: 1,
y: 6,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.TOTAL_DATA_ASSETS,
w: 3,
x: 0,
y: 9,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.FOLLOWING,
w: 1,
x: 3,
y: 1.5,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.RECENTLY_VIEWED,
w: 1,
x: 3,
y: 3,
static: false,
},
];
export const mockCustomizedLayout1: Array<WidgetConfig> = [
{
h: 6,
i: LandingPageWidgetKeys.ACTIVITY_FEED,
w: 3,
x: 0,
y: 0,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.FOLLOWING,
w: 1,
x: 3,
y: 1.5,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.RECENTLY_VIEWED,
w: 1,
x: 3,
y: 3,
static: false,
},
];
export const mockCustomizedLayout2: Array<WidgetConfig> = [
{
h: 6,
i: LandingPageWidgetKeys.ACTIVITY_FEED,
w: 3,
x: 0,
y: 0,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.FOLLOWING,
w: 1,
x: 3,
y: 1.5,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.RECENTLY_VIEWED,
w: 1,
x: 3,
y: 3,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.KPI,
w: 1,
x: 3,
y: 3,
static: false,
},
];
export const mockDocumentData: Document = {
name: `${mockPersonaName}-LandingPage`,
id: 'landing-page-123',
fullyQualifiedName: `persona.${mockPersonaName}.Page.LandingPage`,
entityType: 'Page',
data: {
page: {
layout: mockCustomizedLayout1,
},
},
};
export const mockCustomizePageClassBase = {
defaultLayout: mockDefaultLayout,
};
export const mockShowErrorToast = jest.fn();
export const mockShowSuccessToast = jest.fn();

View File

@ -0,0 +1,159 @@
/*
* 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 React from 'react';
import { LandingPageWidgetKeys } from '../enums/CustomizablePage.enum';
import { Document } from '../generated/entity/docStore/document';
import { Thread, ThreadType } from '../generated/entity/feed/thread';
import { User } from '../generated/entity/teams/user';
import { Paging } from '../generated/type/paging';
import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface';
export const mockPersonaName = 'testPersona';
export const mockUserData: User = {
name: 'Test User',
email: 'testUser1@email.com',
id: '123',
isAdmin: true,
follows: [
{
id: '12',
type: 'table',
},
],
};
export const mockDefaultLayout: Array<WidgetConfig> = [
{
h: 6,
i: LandingPageWidgetKeys.ACTIVITY_FEED,
w: 3,
x: 0,
y: 0,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.MY_DATA,
w: 1,
x: 0,
y: 6,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.KPI,
w: 2,
x: 1,
y: 6,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.TOTAL_DATA_ASSETS,
w: 3,
x: 0,
y: 9,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.FOLLOWING,
w: 1,
x: 3,
y: 1.5,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.RECENTLY_VIEWED,
w: 1,
x: 3,
y: 3,
static: false,
},
];
export const mockCustomizedLayout: Array<WidgetConfig> = [
{
h: 6,
i: LandingPageWidgetKeys.ACTIVITY_FEED,
w: 3,
x: 0,
y: 0,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.FOLLOWING,
w: 1,
x: 3,
y: 1.5,
static: false,
},
{
h: 3,
i: LandingPageWidgetKeys.RECENTLY_VIEWED,
w: 1,
x: 3,
y: 3,
static: false,
},
];
export const mockCustomizePageClassBase = {
defaultLayout: mockDefaultLayout,
announcementWidget: {
h: 3,
i: LandingPageWidgetKeys.ANNOUNCEMENTS,
w: 1,
x: 3,
y: 0,
static: true,
} as WidgetConfig,
landingPageMaxGridSize: 4,
landingPageWidgetMargin: 16,
landingPageRowHeight: 200,
getWidgetsFromKey: (i: string) => () => <div>{i}</div>,
};
export const mockDocumentData: Document = {
name: `${mockPersonaName}-LandingPage`,
fullyQualifiedName: `persona.${mockPersonaName}.Page.LandingPage`,
entityType: 'Page',
data: {
page: {
layout: mockCustomizedLayout,
},
},
};
export const mockAnnouncementsData: Array<Thread> = [
{
id: '444c10b8-e0bb-4e0b-9286-1eeaa2efbf95',
type: ThreadType.Announcement,
about: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>',
message: 'Test Announcement',
announcement: {
description: '',
startTime: 1701620135,
endTime: 1701706538,
},
},
];
export const mockActiveAnnouncementData: { data: Thread[]; paging: Paging } = {
data: mockAnnouncementsData,
paging: { total: 1 },
};

View File

@ -133,7 +133,7 @@ export const CustomPageSettings = () => {
return (
<Row
className="customize-landing-page user-listing p-b-md"
data-testid="user-list-v1-component"
data-testid="custom-page-setting-component"
gutter={[16, 16]}>
<Col span={18}>
<PageHeader data={PAGE_HEADERS.CUSTOM_PAGE} />
@ -152,9 +152,11 @@ export const CustomPageSettings = () => {
<Card
bodyStyle={{ height: '100%' }}
className="h-full"
data-testid={`persona-details-card-${persona.name}`}
extra={
<Button
className="text-link-color"
data-testid="customize-page-button"
size="small"
type="text"
onClick={() => handleCustomisePersona(persona)}>

View File

@ -0,0 +1,231 @@
/*
* 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 { useParams } from 'react-router-dom';
import { LandingPageWidgetKeys } from '../../enums/CustomizablePage.enum';
import { PageType } from '../../generated/system/ui/page';
import {
mockCustomizePageClassBase,
mockDocumentData,
mockPersonaDetails,
mockPersonaName,
mockShowErrorToast,
mockShowSuccessToast,
} from '../../mocks/CustomizablePage.mock';
import { getDocumentByFQN } from '../../rest/DocStoreAPI';
import { getPersonaByName } from '../../rest/PersonaAPI';
import { CustomizablePage } from './CustomizablePage';
import { WidgetConfig } from './CustomizablePage.interface';
jest.mock(
'../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder',
() => {
return jest.fn().mockImplementation(() => <div>ErrorPlaceHolder</div>);
}
);
jest.mock(
'../../components/CustomizableComponents/CustomizeMyData/CustomizeMyData',
() =>
jest
.fn()
.mockImplementation(
({ initialPageData, handleSaveCurrentPageLayout }) => (
<div data-testid="customize-my-data">
{initialPageData.data.page.layout.map((widget: WidgetConfig) => (
<div key={widget.i}>{widget.i}</div>
))}
<div onClick={handleSaveCurrentPageLayout}>
handleSaveCurrentPageLayout
</div>
</div>
)
)
);
jest.mock('../../components/Loader/Loader', () => {
return jest.fn().mockImplementation(() => <div>Loader</div>);
});
jest.mock('../../utils/CustomizePageClassBase', () => {
return mockCustomizePageClassBase;
});
jest.mock('../../rest/DocStoreAPI', () => ({
createDocument: jest
.fn()
.mockImplementation(() => Promise.resolve(mockDocumentData)),
getDocumentByFQN: jest
.fn()
.mockImplementation(() => Promise.resolve(mockDocumentData)),
updateDocument: jest
.fn()
.mockImplementation(() => Promise.resolve(mockDocumentData)),
}));
jest.mock('../../rest/PersonaAPI', () => ({
getPersonaByName: jest
.fn()
.mockImplementation(() => Promise.resolve(mockPersonaDetails)),
}));
jest.mock('../../utils/ToastUtils', () => ({
showErrorToast: mockShowErrorToast,
showSuccessToast: mockShowSuccessToast,
}));
jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockImplementation(() => ({
fqn: mockPersonaName,
pageFqn: PageType.LandingPage,
})),
Link: jest.fn().mockImplementation(() => <div>Link</div>),
}));
describe('CustomizablePage component', () => {
it('CustomizablePage should show ErrorPlaceholder if the API to fetch the persona details fails', async () => {
(getPersonaByName as jest.Mock).mockImplementationOnce(() =>
Promise.reject(new Error('API failure'))
);
await act(async () => {
render(<CustomizablePage />);
});
expect(screen.getByText('ErrorPlaceHolder')).toBeInTheDocument();
expect(screen.queryByTestId('customize-my-data')).toBeNull();
});
it('CustomizablePage should show Loader while the layout is being fetched', async () => {
await act(async () => {
render(<CustomizablePage />);
expect(screen.getByText('Loader')).toBeInTheDocument();
expect(screen.queryByText('ErrorPlaceHolder')).toBeNull();
expect(screen.queryByTestId('customize-my-data')).toBeNull();
});
});
it('CustomizablePage should pass the correct page layout data for the persona', async () => {
await act(async () => {
render(<CustomizablePage />);
});
expect(screen.getByTestId('customize-my-data')).toBeInTheDocument();
expect(screen.queryByText('ErrorPlaceHolder')).toBeNull();
expect(
screen.getByText(LandingPageWidgetKeys.ACTIVITY_FEED)
).toBeInTheDocument();
expect(
screen.getByText(LandingPageWidgetKeys.FOLLOWING)
).toBeInTheDocument();
expect(
screen.getByText(LandingPageWidgetKeys.RECENTLY_VIEWED)
).toBeInTheDocument();
expect(screen.queryByText(LandingPageWidgetKeys.MY_DATA)).toBeNull();
expect(screen.queryByText(LandingPageWidgetKeys.KPI)).toBeNull();
expect(
screen.queryByText(LandingPageWidgetKeys.TOTAL_DATA_ASSETS)
).toBeNull();
});
it('CustomizablePage should pass the default layout data when no layout is present for the persona', async () => {
(getDocumentByFQN as jest.Mock).mockImplementationOnce(() =>
Promise.reject({
response: {
status: 404,
},
})
);
await act(async () => {
render(<CustomizablePage />);
});
expect(screen.getByTestId('customize-my-data')).toBeInTheDocument();
expect(screen.queryByText('ErrorPlaceHolder')).toBeNull();
expect(
screen.getByText(LandingPageWidgetKeys.ACTIVITY_FEED)
).toBeInTheDocument();
expect(
screen.getByText(LandingPageWidgetKeys.FOLLOWING)
).toBeInTheDocument();
expect(
screen.getByText(LandingPageWidgetKeys.RECENTLY_VIEWED)
).toBeInTheDocument();
expect(screen.getByText(LandingPageWidgetKeys.MY_DATA)).toBeInTheDocument();
expect(screen.getByText(LandingPageWidgetKeys.KPI)).toBeInTheDocument();
expect(
screen.getByText(LandingPageWidgetKeys.TOTAL_DATA_ASSETS)
).toBeInTheDocument();
});
it('CustomizablePage should update the layout when layout data is present for persona', async () => {
await act(async () => {
render(<CustomizablePage />);
});
const saveCurrentPageLayoutBtn = screen.getByText(
'handleSaveCurrentPageLayout'
);
await act(async () => {
userEvent.click(saveCurrentPageLayoutBtn);
});
expect(mockShowSuccessToast).toHaveBeenCalledWith(
'server.page-layout-operation-success'
);
});
it('CustomizablePage should save the layout when no layout data present for persona', async () => {
(getDocumentByFQN as jest.Mock).mockImplementationOnce(() =>
Promise.reject({
response: {
status: 404,
},
})
);
await act(async () => {
render(<CustomizablePage />);
});
const saveCurrentPageLayoutBtn = screen.getByText(
'handleSaveCurrentPageLayout'
);
await act(async () => {
userEvent.click(saveCurrentPageLayoutBtn);
});
expect(mockShowSuccessToast).toHaveBeenCalledWith(
'server.page-layout-operation-success'
);
});
it('CustomizablePage should return null for invalid page FQN', async () => {
(useParams as jest.Mock).mockImplementation(() => ({
fqn: mockPersonaName,
pageFqn: 'invalidName',
}));
await act(async () => {
render(<CustomizablePage />);
});
expect(screen.queryByText('ErrorPlaceHolder')).toBeNull();
expect(screen.queryByText('Loader')).toBeNull();
expect(screen.queryByTestId('customize-my-data')).toBeNull();
});
});

View File

@ -24,7 +24,7 @@ import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
} from '../../constants/GlobalSettings.constants';
import { ClientErrors } from '../../enums/axios.enum';
import { ClientErrors } from '../../enums/Axios.enum';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import { EntityType } from '../../enums/entity.enum';
import { Document } from '../../generated/entity/docStore/document';
@ -96,6 +96,8 @@ export const CustomizablePage = () => {
page: { layout: customizePageClassBase.defaultLayout },
},
});
} else {
showErrorToast(error as AxiosError);
}
} finally {
setIsLoading(false);

View File

@ -44,7 +44,7 @@ import './my-data.less';
const ReactGridLayout = WidthProvider(RGL);
const MyDataPageV1 = () => {
const MyDataPage = () => {
const { t } = useTranslation();
const { currentUser } = useAuthContext();
const { selectedPersona } = useApplicationConfigContext();
@ -142,7 +142,15 @@ const MyDataPageV1 = () => {
const widgets = useMemo(
() =>
layout.map((widget) => (
// Adding announcement widget to the layout when announcements are present
// Since the widget wont be in the layout config of the page
// ok
[
...(isEmpty(announcements)
? []
: [customizePageClassBase.announcementWidget]),
...layout,
].map((widget) => (
<div data-grid={widget} key={widget.i}>
{getWidgetFromKey({
announcements: announcements,
@ -214,4 +222,4 @@ const MyDataPageV1 = () => {
);
};
export default MyDataPageV1;
export default MyDataPage;

View File

@ -0,0 +1,275 @@
/*
* 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 { useApplicationConfigContext } from '../../components/ApplicationConfigProvider/ApplicationConfigProvider';
import {
mockActiveAnnouncementData,
mockCustomizePageClassBase,
mockDocumentData,
mockPersonaName,
mockUserData,
} from '../../mocks/MyDataPage.mock';
import { getDocumentByFQN } from '../../rest/DocStoreAPI';
import { getActiveAnnouncement } from '../../rest/feedsAPI';
import MyDataPage from './MyDataPage.component';
const mockLocalStorage = (() => {
let store: Record<string, string> = {};
return {
getItem(key: string) {
return store[key] || '';
},
setItem(key: string, value: string) {
store[key] = value.toString();
},
clear() {
store = {};
},
};
})();
Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage,
});
jest.mock(
'../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider',
() => {
return jest
.fn()
.mockImplementation(({ children }) => (
<div data-testid="activity-feed-provider">{children}</div>
));
}
);
jest.mock('../../components/Loader/Loader', () => {
return jest.fn().mockImplementation(() => <div>Loader</div>);
});
jest.mock('../../utils/CustomizePageClassBase', () => {
return mockCustomizePageClassBase;
});
jest.mock('../../components/PageLayoutV1/PageLayoutV1', () => {
return jest
.fn()
.mockImplementation(({ children }) => (
<div data-testid="page-layout-v1">{children}</div>
));
});
jest.mock('../../components/WelcomeScreen/WelcomeScreen.component', () => {
return jest
.fn()
.mockImplementation(({ onClose }) => (
<div onClick={onClose}>WelcomeScreen</div>
));
});
jest.mock(
'../../components/ApplicationConfigProvider/ApplicationConfigProvider',
() => ({
useApplicationConfigContext: jest
.fn()
.mockImplementation(() => ({ selectedPersona: mockPersonaName })),
})
);
jest.mock('../../components/Auth/AuthProviders/AuthProvider', () => ({
useAuthContext: jest
.fn()
.mockImplementation(() => ({ currentUser: mockUserData })),
}));
jest.mock('../../rest/DocStoreAPI', () => ({
getDocumentByFQN: jest
.fn()
.mockImplementation(() => Promise.resolve(mockDocumentData)),
}));
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: '' })),
}));
jest.mock('react-grid-layout', () => ({
WidthProvider: jest
.fn()
.mockImplementation(() =>
jest
.fn()
.mockImplementation(({ children }) => (
<div data-testid="react-grid-layout">{children}</div>
))
),
__esModule: true,
default: '',
}));
jest.mock('../../hooks/authHooks', () => ({
useAuth: jest.fn().mockImplementation(() => ({ isAuthDisabled: false })),
}));
describe('MyDataPage component', () => {
beforeEach(() => {
localStorage.setItem('loggedInUsers', mockUserData.name);
});
it('MyDataPage should only display WelcomeScreen when user logs in for the first time', async () => {
// Simulate no user is logged in condition
localStorage.clear();
await act(async () => {
render(<MyDataPage />);
});
expect(screen.getByText('WelcomeScreen')).toBeInTheDocument();
expect(screen.queryByTestId('activity-feed-provider')).toBeNull();
});
it('MyDataPage should display the main content after the WelcomeScreen is closed', async () => {
// Simulate no user is logged in condition
localStorage.clear();
await act(async () => {
render(<MyDataPage />);
});
const welcomeScreen = screen.getByText('WelcomeScreen');
expect(welcomeScreen).toBeInTheDocument();
expect(screen.queryByTestId('activity-feed-provider')).toBeNull();
await act(async () => userEvent.click(welcomeScreen));
expect(screen.queryByText('WelcomeScreen')).toBeNull();
expect(screen.getByTestId('activity-feed-provider')).toBeInTheDocument();
expect(screen.getByTestId('react-grid-layout')).toBeInTheDocument();
});
it('MyDataPage should display loader initially while loading data', async () => {
await act(async () => {
render(<MyDataPage />);
expect(screen.queryByText('WelcomeScreen')).toBeNull();
expect(screen.queryByTestId('react-grid-layout')).toBeNull();
expect(screen.getByTestId('activity-feed-provider')).toBeInTheDocument();
expect(screen.getByText('Loader')).toBeInTheDocument();
});
});
it('MyDataPage should display all the widgets in the config and the announcements widget if there are announcements', async () => {
await act(async () => {
render(<MyDataPage />);
});
expect(screen.getByText('KnowledgePanel.ActivityFeed')).toBeInTheDocument();
expect(screen.getByText('KnowledgePanel.Following')).toBeInTheDocument();
expect(
screen.getByText('KnowledgePanel.RecentlyViewed')
).toBeInTheDocument();
expect(
screen.getByText('KnowledgePanel.Announcements')
).toBeInTheDocument();
expect(screen.queryByText('KnowledgePanel.KPI')).toBeNull();
expect(screen.queryByText('KnowledgePanel.TotalAssets')).toBeNull();
expect(screen.queryByText('KnowledgePanel.MyData')).toBeNull();
});
it('MyDataPage should not render announcement widget if there are no announcements', async () => {
(getActiveAnnouncement as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
response: { ...mockActiveAnnouncementData, data: [] },
})
);
await act(async () => {
render(<MyDataPage />);
});
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('MyDataPage should render default widgets when getDocumentByFQN API fails', async () => {
(getDocumentByFQN as jest.Mock).mockImplementationOnce(() =>
Promise.reject(new Error('API failure'))
);
await act(async () => {
render(<MyDataPage />);
});
expect(screen.getByText('KnowledgePanel.ActivityFeed')).toBeInTheDocument();
expect(
screen.getByText('KnowledgePanel.RecentlyViewed')
).toBeInTheDocument();
expect(screen.getByText('KnowledgePanel.Following')).toBeInTheDocument();
expect(
screen.getByText('KnowledgePanel.Announcements')
).toBeInTheDocument();
expect(screen.getByText('KnowledgePanel.KPI')).toBeInTheDocument();
expect(screen.getByText('KnowledgePanel.TotalAssets')).toBeInTheDocument();
expect(screen.getByText('KnowledgePanel.MyData')).toBeInTheDocument();
});
it('MyDataPage should render default widgets when there is no selected persona', async () => {
(useApplicationConfigContext as jest.Mock).mockImplementation(() => ({
selectedPersona: {},
}));
await act(async () => {
render(<MyDataPage />);
});
expect(
await screen.findByText('KnowledgePanel.ActivityFeed')
).toBeInTheDocument();
expect(
await screen.findByText('KnowledgePanel.RecentlyViewed')
).toBeInTheDocument();
expect(
await screen.findByText('KnowledgePanel.Following')
).toBeInTheDocument();
expect(
await screen.findByText('KnowledgePanel.Announcements')
).toBeInTheDocument();
expect(await screen.findByText('KnowledgePanel.KPI')).toBeInTheDocument();
expect(
await screen.findByText('KnowledgePanel.TotalAssets')
).toBeInTheDocument();
expect(
await screen.findByText('KnowledgePanel.MyData')
).toBeInTheDocument();
});
});

View File

@ -231,7 +231,10 @@ export const PersonaDetailsPage = () => {
multiSelect
selectedUsers={personaDetails.users ?? []}
onUpdate={(users) => handlePersonaUpdate({ users })}>
<Button size="small" type="primary">
<Button
data-testid="add-user-button"
size="small"
type="primary">
{t('label.add-entity', { entity: t('label.user') })}
</Button>
</UserSelectableList>

View File

@ -123,7 +123,7 @@ export const PersonaPage = () => {
<Col span={6}>
<Space align="center" className="w-full justify-end" size={16}>
<Button
data-testid="add-user"
data-testid="add-persona-button"
type="primary"
onClick={handleAddNewPersona}>
{t('label.add-entity', { entity: t('label.persona') })}

View File

@ -18,7 +18,7 @@ import { TOUR_SEARCH_TERM } from '../../constants/constants';
import { CurrentTourPageType } from '../../enums/tour.enum';
import { getTourSteps } from '../../utils/TourUtils';
import ExplorePageV1Component from '../ExplorePage/ExplorePageV1.component';
import MyDataPageV1 from '../MyDataPage/MyDataPageV1.component';
import MyDataPage from '../MyDataPage/MyDataPage.component';
import TableDetailsPageV1 from '../TableDetailsPageV1/TableDetailsPageV1';
const TourPage = () => {
@ -41,7 +41,7 @@ const TourPage = () => {
const currentPageComponent = useMemo(() => {
switch (currentTourPage) {
case CurrentTourPageType.MY_DATA_PAGE:
return <MyDataPageV1 />;
return <MyDataPage />;
case CurrentTourPageType.EXPLORE_PAGE:
return <ExplorePageV1Component />;

View File

@ -12,7 +12,14 @@
*/
import i18next from 'i18next';
import { capitalize, isEmpty, isUndefined, max, uniqueId } from 'lodash';
import {
capitalize,
isEmpty,
isUndefined,
max,
uniqBy,
uniqueId,
} from 'lodash';
import React from 'react';
import { Layout } from 'react-grid-layout';
import EmptyWidgetPlaceholder from '../components/CustomizableComponents/EmptyWidgetPlaceholder/EmptyWidgetPlaceholder';
@ -190,7 +197,7 @@ export const getWidgetFromKey = ({
handleOpenAddWidgetModal?: () => void;
handlePlaceholderWidgetKey?: (key: string) => void;
handleRemoveWidget?: (key: string) => void;
announcements: Thread[];
announcements?: Thread[];
followedData?: EntityReference[];
followedDataCount: number;
isLoadingOwnedData: boolean;
@ -250,3 +257,14 @@ export const getLayoutWithEmptyWidgetPlaceholder = (
isDraggable: false,
},
];
// Function to filter out empty widget placeholders and only keep knowledge panels
export const getUniqueFilteredLayout = (layout: WidgetConfig[]) =>
uniqBy(
layout.filter(
(widget) =>
widget.i.startsWith('KnowledgePanel') &&
!widget.i.endsWith('.EmptyWidgetPlaceholder')
),
'i'
);

View File

@ -57,6 +57,15 @@ class CustomizePageClassBase {
totalAssets: 3,
};
announcementWidget: WidgetConfig = {
h: this.landingPageWidgetDefaultHeights.announcements,
i: LandingPageWidgetKeys.ANNOUNCEMENTS,
w: 1,
x: 3,
y: 0,
static: true, // Making announcement widget fixed on top right position
};
defaultLayout: Array<WidgetConfig> = [
{
h: this.landingPageWidgetDefaultHeights.activityFeed,
@ -90,14 +99,6 @@ class CustomizePageClassBase {
y: 9,
static: false,
},
{
h: this.landingPageWidgetDefaultHeights.announcements,
i: LandingPageWidgetKeys.ANNOUNCEMENTS,
w: 1,
x: 3,
y: 0,
static: false,
},
{
h: this.landingPageWidgetDefaultHeights.following,
i: LandingPageWidgetKeys.FOLLOWING,

View File

@ -24,7 +24,7 @@ import RichTextEditorPreviewer from '../components/common/RichTextEditor/RichTex
import Loader from '../components/Loader/Loader';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import { getExplorePath } from '../constants/constants';
import { SettledStatus } from '../enums/axios.enum';
import { SettledStatus } from '../enums/Axios.enum';
import { ExplorePageTabs } from '../enums/Explore.enum';
import { SearchIndex } from '../enums/search.enum';
import { Classification } from '../generated/entity/classification/classification';

View File

@ -15,7 +15,7 @@ import { AxiosError } from 'axios';
import { isEmpty, isString } from 'lodash';
import React from 'react';
import { toast } from 'react-toastify';
import { ClientErrors } from '../enums/axios.enum';
import { ClientErrors } from '../enums/Axios.enum';
import i18n from './i18next/LocalUtil';
import { getErrorText } from './StringsUtils';

View File

@ -14,7 +14,7 @@
import { isEqual } from 'lodash';
import { OidcUser } from '../components/Auth/AuthProviders/AuthProvider.interface';
import { WILD_CARD_CHAR } from '../constants/char.constants';
import { SettledStatus } from '../enums/axios.enum';
import { SettledStatus } from '../enums/Axios.enum';
import { SearchIndex } from '../enums/search.enum';
import { SearchResponse } from '../interface/search.interface';
import { getSearchedTeams, getSearchedUsers } from '../rest/miscAPI';