mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-26 09:55:52 +00:00
MINOR: fix the widget placement when adding new one (#17329)
* fix the widget placement when adding new one * fix the widget not adding in the right side add placeholders * added unit test for the same * fix sonar
This commit is contained in:
parent
a098c20c7c
commit
785e450e28
@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { expect, Page, test as base } from '@playwright/test';
|
||||
import { PersonaClass } from '../../support/persona/PersonaClass';
|
||||
import { UserClass } from '../../support/user/UserClass';
|
||||
import { performAdminLogin } from '../../utils/admin';
|
||||
import { redirectToHomePage, toastNotification } from '../../utils/common';
|
||||
import {
|
||||
checkAllDefaultWidgets,
|
||||
navigateToCustomizeLandingPage,
|
||||
removeAndCheckWidget,
|
||||
setUserDefaultPersona,
|
||||
} from '../../utils/customizeLandingPage';
|
||||
|
||||
const adminUser = new UserClass();
|
||||
const persona = new PersonaClass();
|
||||
const persona2 = new PersonaClass();
|
||||
|
||||
const test = base.extend<{ adminPage: Page; userPage: Page }>({
|
||||
adminPage: async ({ browser }, use) => {
|
||||
const adminPage = await browser.newPage();
|
||||
await adminUser.login(adminPage);
|
||||
await use(adminPage);
|
||||
await adminPage.close();
|
||||
},
|
||||
});
|
||||
|
||||
base.beforeAll('Setup pre-requests', async ({ browser }) => {
|
||||
const { afterAction, apiContext } = await performAdminLogin(browser);
|
||||
await adminUser.create(apiContext);
|
||||
await adminUser.setAdminRole(apiContext);
|
||||
await persona.create(apiContext, [adminUser.responseData.id]);
|
||||
await persona2.create(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
base.afterAll('Cleanup', async ({ browser }) => {
|
||||
const { afterAction, apiContext } = await performAdminLogin(browser);
|
||||
await adminUser.delete(apiContext);
|
||||
await persona.delete(apiContext);
|
||||
await persona2.delete(apiContext);
|
||||
await afterAction();
|
||||
});
|
||||
|
||||
test.describe('Customize Landing Page Flow', () => {
|
||||
test('Check all default widget present', async ({ adminPage }) => {
|
||||
await redirectToHomePage(adminPage);
|
||||
await adminPage.getByTestId('welcome-screen-close-btn').click();
|
||||
await checkAllDefaultWidgets(adminPage);
|
||||
});
|
||||
|
||||
test('Remove and check widget', async ({ adminPage }) => {
|
||||
await redirectToHomePage(adminPage);
|
||||
await setUserDefaultPersona(adminPage, persona.responseData.displayName);
|
||||
await navigateToCustomizeLandingPage(adminPage, {
|
||||
personaName: persona.responseData.name,
|
||||
customPageDataResponse: 404,
|
||||
});
|
||||
|
||||
await removeAndCheckWidget(adminPage, {
|
||||
widgetTestId: 'activity-feed-widget',
|
||||
widgetKey: 'KnowledgePanel.ActivityFeed',
|
||||
});
|
||||
await removeAndCheckWidget(adminPage, {
|
||||
widgetTestId: 'following-widget',
|
||||
widgetKey: 'KnowledgePanel.Following',
|
||||
});
|
||||
await removeAndCheckWidget(adminPage, {
|
||||
widgetTestId: 'kpi-widget',
|
||||
widgetKey: 'KnowledgePanel.KPI',
|
||||
});
|
||||
|
||||
const saveResponse = adminPage.waitForResponse('/api/v1/docStore');
|
||||
await adminPage.click('[data-testid="save-button"]');
|
||||
await saveResponse;
|
||||
|
||||
await toastNotification(adminPage, 'Page layout created successfully.');
|
||||
await redirectToHomePage(adminPage);
|
||||
|
||||
// Check if removed widgets are not present on landing adminPage
|
||||
await expect(
|
||||
adminPage.locator('[data-testid="activity-feed-widget"]')
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
adminPage.locator('[data-testid="following-widget"]')
|
||||
).not.toBeVisible();
|
||||
await expect(
|
||||
adminPage.locator('[data-testid="kpi-widget"]')
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Remove and add the widget in the same placeholder', async ({
|
||||
adminPage,
|
||||
}) => {
|
||||
await redirectToHomePage(adminPage);
|
||||
|
||||
const feedResponse = adminPage.waitForResponse(
|
||||
'/api/v1/feed?type=Conversation&*'
|
||||
);
|
||||
await navigateToCustomizeLandingPage(adminPage, {
|
||||
personaName: persona2.responseData.name,
|
||||
customPageDataResponse: 404,
|
||||
});
|
||||
|
||||
await feedResponse;
|
||||
|
||||
await adminPage.waitForSelector('[data-testid="activity-feed-widget"]');
|
||||
|
||||
const followingElementStyle = await adminPage
|
||||
.locator('[id="KnowledgePanel.Following"]')
|
||||
.evaluate((node) => {
|
||||
const computedStyle = window.getComputedStyle(node);
|
||||
|
||||
return {
|
||||
transform: computedStyle.transform,
|
||||
};
|
||||
});
|
||||
|
||||
// Remove and check the placement of Following widget.
|
||||
await adminPage.click(
|
||||
'[data-testid="following-widget"] [data-testid="remove-widget-button"]'
|
||||
);
|
||||
|
||||
await adminPage.waitForSelector('[data-testid="following-widget"]', {
|
||||
state: 'detached',
|
||||
});
|
||||
|
||||
await adminPage.waitForSelector(
|
||||
'[data-testid*="KnowledgePanel.Following"][data-testid$="EmptyWidgetPlaceholder"]'
|
||||
);
|
||||
|
||||
// Add KPI widget in the same placeholder
|
||||
const getWidgetList = adminPage.waitForResponse(
|
||||
'api/v1/docStore?fqnPrefix=KnowledgePanel&*'
|
||||
);
|
||||
await adminPage.click(
|
||||
'[data-testid="KnowledgePanel.Following.EmptyWidgetPlaceholder"] [data-testid="add-widget-button"]'
|
||||
);
|
||||
|
||||
await getWidgetList;
|
||||
|
||||
await adminPage.waitForSelector('[role="dialog"].ant-modal');
|
||||
|
||||
expect(adminPage.locator('[role="dialog"].ant-modal')).toBeVisible();
|
||||
|
||||
await adminPage.click('[data-testid="KPI-widget-tab-label"]');
|
||||
|
||||
await adminPage
|
||||
.locator('.ant-tabs-tabpane-active [data-testid="add-widget-button"]')
|
||||
.click();
|
||||
|
||||
await adminPage.waitForSelector('[role="dialog"].ant-modal', {
|
||||
state: 'detached',
|
||||
});
|
||||
|
||||
const kpiElement = adminPage.locator('[id^="KnowledgePanel.KPI-"]');
|
||||
const kpiElementStyle = await kpiElement.evaluate((node) => {
|
||||
const computedStyle = window.getComputedStyle(node);
|
||||
|
||||
return {
|
||||
transform: computedStyle.transform,
|
||||
};
|
||||
});
|
||||
|
||||
// Check if the KPI widget is added in the same placeholder,by their transform property or placement.
|
||||
expect(kpiElementStyle.transform).toEqual(followingElementStyle.transform);
|
||||
|
||||
const saveResponse = adminPage.waitForResponse('/api/v1/docStore');
|
||||
await adminPage.click('[data-testid="save-button"]');
|
||||
await saveResponse;
|
||||
|
||||
await toastNotification(adminPage, 'Page layout created successfully.');
|
||||
});
|
||||
});
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { APIRequestContext } from '@playwright/test';
|
||||
import { uuid } from '../../utils/common';
|
||||
|
||||
type ResponseDataType = {
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
id?: string;
|
||||
fullyQualifiedName?: string;
|
||||
users?: string[];
|
||||
};
|
||||
|
||||
export class PersonaClass {
|
||||
id = uuid();
|
||||
data: ResponseDataType;
|
||||
responseData: ResponseDataType;
|
||||
|
||||
constructor(data?: ResponseDataType) {
|
||||
this.data = data ?? {
|
||||
name: `PW%Persona-${this.id}`,
|
||||
displayName: `PW Persona ${this.id}`,
|
||||
description: 'playwright for persona description',
|
||||
users: [],
|
||||
};
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.responseData;
|
||||
}
|
||||
|
||||
async create(apiContext: APIRequestContext, users?: string[]) {
|
||||
const response = await apiContext.post('/api/v1/personas', {
|
||||
data: { ...this.data, users },
|
||||
});
|
||||
const data = await response.json();
|
||||
this.responseData = data;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async delete(apiContext: APIRequestContext) {
|
||||
const response = await apiContext.delete(
|
||||
`/api/v1/personas/${this.responseData.id}?hardDelete=true&recursive=false`
|
||||
);
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async patch(apiContext: APIRequestContext, data: Record<string, unknown>[]) {
|
||||
const response = await apiContext.patch(
|
||||
`/api/v1/personas/${this.responseData.id}`,
|
||||
{
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json-patch+json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.responseData = await response.json();
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
}
|
@ -61,6 +61,19 @@ export class UserClass {
|
||||
};
|
||||
}
|
||||
|
||||
async setAdminRole(apiContext: APIRequestContext) {
|
||||
return this.patch({
|
||||
apiContext,
|
||||
patchData: [
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/isAdmin',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async delete(apiContext: APIRequestContext) {
|
||||
const response = await apiContext.delete(
|
||||
`/api/v1/users/${this.responseData.id}?recursive=false&hardDelete=true`
|
||||
|
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { expect, Page } from '@playwright/test';
|
||||
import { GlobalSettingOptions } from '../constant/settings';
|
||||
import { visitUserProfilePage } from './common';
|
||||
import { settingClick } from './sidebar';
|
||||
|
||||
export const navigateToCustomizeLandingPage = async (
|
||||
page: Page,
|
||||
{ personaName, customPageDataResponse }
|
||||
) => {
|
||||
const getPersonas = page.waitForResponse('/api/v1/personas*');
|
||||
|
||||
await settingClick(page, GlobalSettingOptions.CUSTOMIZE_LANDING_PAGE);
|
||||
|
||||
await getPersonas;
|
||||
|
||||
// Navigate to the customize landing page
|
||||
await page.click(
|
||||
`[data-testid="persona-details-card-${personaName}"] [data-testid="customize-page-button"]`
|
||||
);
|
||||
|
||||
const getCustomPageDataResponse = await page.waitForResponse(
|
||||
`/api/v1/docStore/name/persona.${encodeURIComponent(
|
||||
personaName
|
||||
)}.Page.LandingPage`
|
||||
);
|
||||
|
||||
// await getCustomPageDataResponse;
|
||||
expect(getCustomPageDataResponse.status()).toBe(customPageDataResponse);
|
||||
};
|
||||
|
||||
export const removeAndCheckWidget = async (
|
||||
page: Page,
|
||||
{ widgetTestId, widgetKey }
|
||||
) => {
|
||||
// Click on remove widget button
|
||||
await page.click(
|
||||
`[data-testid="${widgetTestId}"] [data-testid="remove-widget-button"]`
|
||||
);
|
||||
|
||||
// Check if widget does not exist
|
||||
await page.waitForSelector(`[data-testid="${widgetTestId}"]`, {
|
||||
state: 'detached',
|
||||
});
|
||||
|
||||
// Check if empty widget placeholder is displayed in place of removed widget
|
||||
await page.waitForSelector(
|
||||
`[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"]`
|
||||
);
|
||||
|
||||
// Remove empty widget placeholder
|
||||
await page.click(
|
||||
`[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"] [data-testid="remove-widget-button"]`
|
||||
);
|
||||
|
||||
// Check if empty widget placeholder does not exist
|
||||
await page.waitForSelector(
|
||||
`[data-testid*="${widgetKey}"][data-testid$="EmptyWidgetPlaceholder"]`,
|
||||
{ state: 'detached' }
|
||||
);
|
||||
};
|
||||
|
||||
export const checkAllDefaultWidgets = async (
|
||||
page: Page,
|
||||
checkEmptyWidgetPlaceholder = false
|
||||
) => {
|
||||
await expect(page.getByTestId('activity-feed-widget')).toBeVisible();
|
||||
await expect(page.getByTestId('following-widget')).toBeVisible();
|
||||
await expect(page.getByTestId('recently-viewed-widget')).toBeVisible();
|
||||
await expect(page.getByTestId('data-assets-widget')).toBeVisible();
|
||||
await expect(page.getByTestId('my-data-widget')).toBeVisible();
|
||||
await expect(page.getByTestId('kpi-widget')).toBeVisible();
|
||||
await expect(page.getByTestId('total-assets-widget')).toBeVisible();
|
||||
|
||||
if (checkEmptyWidgetPlaceholder) {
|
||||
await expect(
|
||||
page.getByTestId('ExtraWidget.EmptyWidgetPlaceholder')
|
||||
).toBeVisible();
|
||||
}
|
||||
};
|
||||
|
||||
export const setUserDefaultPersona = async (
|
||||
page: Page,
|
||||
personaName: string
|
||||
) => {
|
||||
await visitUserProfilePage(page);
|
||||
|
||||
await page
|
||||
.locator(
|
||||
'[data-testid="user-profile-details"] [data-testid="edit-persona"]'
|
||||
)
|
||||
.click();
|
||||
|
||||
await page.waitForSelector(
|
||||
'[role="tooltip"] [data-testid="selectable-list"]'
|
||||
);
|
||||
|
||||
const setDefaultPersona = page.waitForResponse('/api/v1/users/*');
|
||||
|
||||
await page.getByTitle(personaName).click();
|
||||
|
||||
await setDefaultPersona;
|
||||
|
||||
await expect(
|
||||
page.locator('[data-testid="user-profile-details"]')
|
||||
).toContainText(personaName);
|
||||
};
|
@ -90,7 +90,6 @@ function AddWidgetTabContent({
|
||||
placement="bottom"
|
||||
title={widgetAddable ? '' : t('message.can-not-add-widget')}>
|
||||
<Button
|
||||
ghost
|
||||
className="p-x-lg m-t-md"
|
||||
data-testid="add-widget-button"
|
||||
disabled={!widgetAddable}
|
||||
|
@ -208,5 +208,42 @@ export const mockAddWidgetReturnValues = [
|
||||
x: 0,
|
||||
y: 100,
|
||||
},
|
||||
{ h: 3, i: 'KnowledgePanel.Following-1', static: false, w: 1, x: 0, y: 6 },
|
||||
{ h: 3, i: 'KnowledgePanel.Following-1', static: false, w: 1, x: 0, y: 4 },
|
||||
];
|
||||
|
||||
export const mockAddWidgetReturnValues2 = [
|
||||
{
|
||||
h: 6,
|
||||
i: 'KnowledgePanel.ActivityFeed',
|
||||
static: false,
|
||||
w: 3,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
h: 3,
|
||||
i: 'KnowledgePanel.RecentlyViewed',
|
||||
static: false,
|
||||
w: 1,
|
||||
x: 3,
|
||||
y: 3,
|
||||
},
|
||||
{
|
||||
h: 2,
|
||||
i: 'ExtraWidget.EmptyWidgetPlaceholder',
|
||||
isDraggable: false,
|
||||
static: false,
|
||||
w: 4,
|
||||
x: 0,
|
||||
y: 100,
|
||||
},
|
||||
{
|
||||
h: 3,
|
||||
i: 'KnowledgePanel.dataAsset',
|
||||
w: 1,
|
||||
x: 2,
|
||||
y: 4,
|
||||
static: false,
|
||||
},
|
||||
{ h: 3, i: 'KnowledgePanel.Following-2', static: false, w: 1, x: 3, y: 4 },
|
||||
];
|
||||
|
@ -13,19 +13,41 @@
|
||||
import { mockWidget } from '../mocks/AddWidgetTabContent.mock';
|
||||
import {
|
||||
mockAddWidgetReturnValues,
|
||||
mockAddWidgetReturnValues2,
|
||||
mockCurrentAddWidget,
|
||||
} from '../mocks/CustomizablePage.mock';
|
||||
import { getAddWidgetHandler } from './CustomizableLandingPageUtils';
|
||||
|
||||
describe('getAddWidgetHandler function', () => {
|
||||
it('should add new widget at EmptyWidgetPlaceholder place to be in the bottom', () => {
|
||||
it('should add new widget at the bottom if not fit in the grid row', () => {
|
||||
const result = getAddWidgetHandler(
|
||||
mockWidget,
|
||||
'ExtraWidget.EmptyWidgetPlaceholder',
|
||||
1,
|
||||
3
|
||||
4
|
||||
)(mockCurrentAddWidget);
|
||||
|
||||
expect(result).toEqual(mockAddWidgetReturnValues);
|
||||
});
|
||||
|
||||
it('should add new widget at the same line if new widget can fit', () => {
|
||||
const result = getAddWidgetHandler(
|
||||
mockWidget,
|
||||
'ExtraWidget.EmptyWidgetPlaceholder',
|
||||
1,
|
||||
4
|
||||
)([
|
||||
...mockCurrentAddWidget,
|
||||
{
|
||||
h: 3,
|
||||
i: 'KnowledgePanel.dataAsset',
|
||||
w: 1,
|
||||
x: 2,
|
||||
y: 4,
|
||||
static: false,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result).toEqual(mockAddWidgetReturnValues2);
|
||||
});
|
||||
});
|
||||
|
@ -34,6 +34,47 @@ import { EntityReference } from '../generated/entity/type';
|
||||
import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface';
|
||||
import customizePageClassBase from './CustomizePageClassBase';
|
||||
|
||||
const getNewWidgetPlacement = (
|
||||
currentLayout: WidgetConfig[],
|
||||
widgetWidth: number
|
||||
) => {
|
||||
const lowestWidgetLayout = currentLayout.reduce(
|
||||
(acc, widget) => {
|
||||
if (
|
||||
widget.y >= acc.y &&
|
||||
widget.i !== LandingPageWidgetKeys.EMPTY_WIDGET_PLACEHOLDER
|
||||
) {
|
||||
if (widget.y === acc.y && widget.x < acc.x) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ y: 0, x: 0, w: 0 }
|
||||
);
|
||||
|
||||
// Check if there's enough space to place the new widget on the same row
|
||||
if (
|
||||
customizePageClassBase.landingPageMaxGridSize -
|
||||
(lowestWidgetLayout.x + lowestWidgetLayout.w) >=
|
||||
widgetWidth
|
||||
) {
|
||||
return {
|
||||
x: lowestWidgetLayout.x + lowestWidgetLayout.w,
|
||||
y: lowestWidgetLayout.y,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, move to the next row
|
||||
return {
|
||||
x: 0,
|
||||
y: lowestWidgetLayout.y + 1,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAddWidgetHandler =
|
||||
(
|
||||
newWidgetData: Document,
|
||||
@ -55,19 +96,14 @@ export const getAddWidgetHandler =
|
||||
if (
|
||||
placeholderWidgetKey === LandingPageWidgetKeys.EMPTY_WIDGET_PLACEHOLDER
|
||||
) {
|
||||
const emptyWidgetPlaceholder = currentLayout.find(
|
||||
(item) => item.i === LandingPageWidgetKeys.EMPTY_WIDGET_PLACEHOLDER
|
||||
) ?? { x: 0, y: 99 };
|
||||
|
||||
return [
|
||||
...moveEmptyWidgetToTheEnd(currentLayout),
|
||||
{
|
||||
w: widgetWidth,
|
||||
h: widgetHeight,
|
||||
x: emptyWidgetPlaceholder.x,
|
||||
y: emptyWidgetPlaceholder.y,
|
||||
i: widgetFQN,
|
||||
static: false,
|
||||
...getNewWidgetPlacement(currentLayout, widgetWidth),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
|
@ -47,7 +47,7 @@ class CustomizePageClassBase {
|
||||
defaultWidgetHeight = 3;
|
||||
landingPageWidgetMargin = 16;
|
||||
landingPageRowHeight = 100;
|
||||
landingPageMaxGridSize = 3;
|
||||
landingPageMaxGridSize = 4;
|
||||
|
||||
landingPageWidgetDefaultHeights: Record<string, number> = {
|
||||
activityFeed: 6,
|
||||
|
Loading…
x
Reference in New Issue
Block a user