mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-24 00:48:36 +00:00
* fix the wrong suggestion count being displayed on description * added button to generate all the suggestions * minor placement of button change * added some unit test * added playwright test for it * minor changes * change the button design
This commit is contained in:
parent
4dbbc294bf
commit
fe7ce3d11a
@ -0,0 +1,251 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import test, { expect } from '@playwright/test';
|
||||||
|
import { TableClass } from '../../support/entity/TableClass';
|
||||||
|
import { UserClass } from '../../support/user/UserClass';
|
||||||
|
import { performAdminLogin } from '../../utils/admin';
|
||||||
|
import { redirectToHomePage } from '../../utils/common';
|
||||||
|
import { createTableSuggestions } from '../../utils/suggestions';
|
||||||
|
import { performUserLogin } from '../../utils/user';
|
||||||
|
|
||||||
|
const table = new TableClass();
|
||||||
|
const user1 = new UserClass();
|
||||||
|
const user2 = new UserClass();
|
||||||
|
let entityLinkList: string[];
|
||||||
|
|
||||||
|
test.describe('Description Suggestions Table Entity', () => {
|
||||||
|
test.slow(true);
|
||||||
|
|
||||||
|
test.beforeAll('Setup pre-requests', async ({ browser }) => {
|
||||||
|
const { afterAction, apiContext } = await performAdminLogin(browser);
|
||||||
|
await table.create(apiContext);
|
||||||
|
|
||||||
|
entityLinkList = table.entityLinkColumnsName.map(
|
||||||
|
(entityLinkName) =>
|
||||||
|
`<#E::table::${table.entityResponseData.fullyQualifiedName}::columns::${entityLinkName}>`
|
||||||
|
);
|
||||||
|
await user1.create(apiContext);
|
||||||
|
await user2.create(apiContext);
|
||||||
|
|
||||||
|
// Create suggestions for both users
|
||||||
|
for (const entityLink of entityLinkList) {
|
||||||
|
await createTableSuggestions(apiContext, entityLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Cleanup', async ({ browser }) => {
|
||||||
|
const { afterAction, apiContext } = await performAdminLogin(browser);
|
||||||
|
await table.delete(apiContext);
|
||||||
|
await user1.delete(apiContext);
|
||||||
|
await user2.delete(apiContext);
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('View, Close, Reject and Accept the Suggestions', async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
const { page, afterAction } = await performAdminLogin(browser);
|
||||||
|
|
||||||
|
await test.step('View and Open the Suggestions', async () => {
|
||||||
|
await redirectToHomePage(page);
|
||||||
|
await table.visitEntityPage(page);
|
||||||
|
|
||||||
|
await expect(page.getByText('Suggested Descriptions')).toBeVisible();
|
||||||
|
|
||||||
|
const allAvatarSuggestion = page
|
||||||
|
.getByTestId('asset-description-container')
|
||||||
|
.getByTestId('profile-avatar');
|
||||||
|
|
||||||
|
// Two users profile will be visible, 3rd one will come after AllFetch is clicked
|
||||||
|
await expect(allAvatarSuggestion).toHaveCount(1);
|
||||||
|
|
||||||
|
// Click the first avatar
|
||||||
|
await allAvatarSuggestion.nth(0).click();
|
||||||
|
|
||||||
|
// Actions Buttons should be visible
|
||||||
|
await expect(page.getByTestId('accept-all-suggestions')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('reject-all-suggestions')).toBeVisible();
|
||||||
|
await expect(page.getByTestId('close-suggestion')).toBeVisible();
|
||||||
|
|
||||||
|
// All Column Suggestions Card should be visible
|
||||||
|
await expect(page.locator('.suggested-description-card')).toHaveCount(6);
|
||||||
|
|
||||||
|
// Close the suggestions
|
||||||
|
await page.getByTestId('close-suggestion').click();
|
||||||
|
|
||||||
|
await expect(allAvatarSuggestion).toHaveCount(1); // suggestion should not reject or disappear
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Accept Single Suggestion', async () => {
|
||||||
|
const allAvatarSuggestion = page
|
||||||
|
.getByTestId('asset-description-container')
|
||||||
|
.getByTestId('profile-avatar');
|
||||||
|
|
||||||
|
// Click the first avatar
|
||||||
|
await allAvatarSuggestion.nth(0).click();
|
||||||
|
|
||||||
|
const singleResolveResponse = page.waitForResponse(
|
||||||
|
'/api/v1/suggestions/*/accept'
|
||||||
|
);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
`[data-row-key*=${table.columnsName[0]}] [data-testid="accept-suggestion"]`
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await singleResolveResponse;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.locator(
|
||||||
|
`[data-row-key*=${table.columnsName[0]}] [data-testid="description"]`
|
||||||
|
)
|
||||||
|
).toContainText('this is suggested data description');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Reject Single Suggestion', async () => {
|
||||||
|
const allAvatarSuggestion = page
|
||||||
|
.getByTestId('asset-description-container')
|
||||||
|
.getByTestId('profile-avatar');
|
||||||
|
|
||||||
|
// Click the first avatar
|
||||||
|
await allAvatarSuggestion.nth(0).click();
|
||||||
|
|
||||||
|
const singleResolveResponse = page.waitForResponse(
|
||||||
|
'/api/v1/suggestions/*/reject'
|
||||||
|
);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator(
|
||||||
|
`[data-row-key*=${table.columnsName[1]}] [data-testid="reject-suggestion"]`
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await singleResolveResponse;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.locator(
|
||||||
|
`[data-row-key*=${table.columnsName[1]}] [data-testid="description"]`
|
||||||
|
)
|
||||||
|
).not.toContainText('this is suggested data description');
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Accept all Suggestion', async () => {
|
||||||
|
const allAvatarSuggestion = page
|
||||||
|
.getByTestId('asset-description-container')
|
||||||
|
.getByTestId('profile-avatar');
|
||||||
|
|
||||||
|
// Click the first avatar
|
||||||
|
await allAvatarSuggestion.nth(0).click();
|
||||||
|
|
||||||
|
const acceptResponse = page.waitForResponse(
|
||||||
|
'/api/v1/suggestions/accept-all?userId=*&entityFQN=*&suggestionType=SuggestDescription'
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.click(`[data-testid="accept-all-suggestions"]`);
|
||||||
|
|
||||||
|
await acceptResponse;
|
||||||
|
|
||||||
|
// check the third column description, since other two are already checked
|
||||||
|
await expect(
|
||||||
|
page.locator(
|
||||||
|
`[data-row-key*=${table.columnsName[5]}] [data-testid="description"]`
|
||||||
|
)
|
||||||
|
).toContainText('this is suggested data description');
|
||||||
|
|
||||||
|
// Actions Buttons should not be visible
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('accept-all-suggestions')
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('reject-all-suggestions')
|
||||||
|
).not.toBeVisible();
|
||||||
|
await expect(page.getByTestId('close-suggestion')).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reject All Suggestions', async ({ browser }) => {
|
||||||
|
const { page, afterAction } = await performAdminLogin(browser);
|
||||||
|
|
||||||
|
await redirectToHomePage(page);
|
||||||
|
await table.visitEntityPage(page);
|
||||||
|
|
||||||
|
const allAvatarSuggestion = page
|
||||||
|
.getByTestId('asset-description-container')
|
||||||
|
.getByTestId('profile-avatar');
|
||||||
|
|
||||||
|
// Click the first avatar
|
||||||
|
await allAvatarSuggestion.nth(0).click();
|
||||||
|
|
||||||
|
const acceptResponse = page.waitForResponse(
|
||||||
|
'/api/v1/suggestions/reject-all?userId=*&entityFQN=*&suggestionType=SuggestDescription'
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.click(`[data-testid="reject-all-suggestions"]`);
|
||||||
|
|
||||||
|
await acceptResponse;
|
||||||
|
|
||||||
|
// check the last column description
|
||||||
|
await expect(
|
||||||
|
page.locator(
|
||||||
|
`[data-row-key*=${table.columnsName[1]}] [data-testid="description"]`
|
||||||
|
)
|
||||||
|
).not.toContainText('this is suggested data description');
|
||||||
|
|
||||||
|
// Actions Buttons should not be visible
|
||||||
|
await expect(page.getByTestId('accept-all-suggestions')).not.toBeVisible();
|
||||||
|
await expect(page.getByTestId('reject-all-suggestions')).not.toBeVisible();
|
||||||
|
await expect(page.getByTestId('close-suggestion')).not.toBeVisible();
|
||||||
|
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fetch All Pending Suggestions', async ({ browser }) => {
|
||||||
|
const { page, afterAction } = await performAdminLogin(browser);
|
||||||
|
const { afterAction: afterAction2, apiContext: apiContext2 } =
|
||||||
|
await performUserLogin(browser, user1);
|
||||||
|
const { afterAction: afterAction3, apiContext: apiContext3 } =
|
||||||
|
await performUserLogin(browser, user2);
|
||||||
|
|
||||||
|
for (const entityLink of entityLinkList) {
|
||||||
|
await createTableSuggestions(apiContext2, entityLink);
|
||||||
|
await createTableSuggestions(apiContext3, entityLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
await redirectToHomePage(page);
|
||||||
|
await table.visitEntityPage(page);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('more-suggestion-button')).toBeVisible();
|
||||||
|
|
||||||
|
const fetchMoreSuggestionResponse = page.waitForResponse(
|
||||||
|
'/api/v1/suggestions?entityFQN=*&limit=*'
|
||||||
|
);
|
||||||
|
await page.getByTestId('more-suggestion-button').click();
|
||||||
|
await fetchMoreSuggestionResponse;
|
||||||
|
|
||||||
|
const allAvatarSuggestion = page
|
||||||
|
.getByTestId('asset-description-container')
|
||||||
|
.getByTestId('profile-avatar');
|
||||||
|
|
||||||
|
// Click the first avatar
|
||||||
|
await expect(allAvatarSuggestion).toHaveCount(3);
|
||||||
|
|
||||||
|
await afterAction();
|
||||||
|
await afterAction2();
|
||||||
|
await afterAction3();
|
||||||
|
});
|
||||||
|
});
|
@ -54,30 +54,47 @@ export class TableClass extends EntityClass {
|
|||||||
name: `pw-database-schema-${uuid()}`,
|
name: `pw-database-schema-${uuid()}`,
|
||||||
database: `${this.service.name}.${this.database.name}`,
|
database: `${this.service.name}.${this.database.name}`,
|
||||||
};
|
};
|
||||||
|
columnsName = [
|
||||||
|
`user_id${uuid()}`,
|
||||||
|
`shop_id${uuid()}`,
|
||||||
|
`name${uuid()}`,
|
||||||
|
`first_name${uuid()}`,
|
||||||
|
`last_name${uuid()}`,
|
||||||
|
`email${uuid()}`,
|
||||||
|
];
|
||||||
|
entityLinkColumnsName = [
|
||||||
|
this.columnsName[0],
|
||||||
|
this.columnsName[1],
|
||||||
|
this.columnsName[2],
|
||||||
|
`"${this.columnsName[2]}.${this.columnsName[3]}"`,
|
||||||
|
`"${this.columnsName[2]}.${this.columnsName[4]}"`,
|
||||||
|
this.columnsName[5],
|
||||||
|
];
|
||||||
|
|
||||||
children = [
|
children = [
|
||||||
{
|
{
|
||||||
name: `user_id${uuid()}`,
|
name: this.columnsName[0],
|
||||||
dataType: 'NUMERIC',
|
dataType: 'NUMERIC',
|
||||||
dataTypeDisplay: 'numeric',
|
dataTypeDisplay: 'numeric',
|
||||||
description:
|
description:
|
||||||
'Unique identifier for the user of your Shopify POS or your Shopify admin.',
|
'Unique identifier for the user of your Shopify POS or your Shopify admin.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `shop_id${uuid()}`,
|
name: this.columnsName[1],
|
||||||
dataType: 'NUMERIC',
|
dataType: 'NUMERIC',
|
||||||
dataTypeDisplay: 'numeric',
|
dataTypeDisplay: 'numeric',
|
||||||
description:
|
description:
|
||||||
'The ID of the store. This column is a foreign key reference to the shop_id column in the dim.shop table.',
|
'The ID of the store. This column is a foreign key reference to the shop_id column in the dim.shop table.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `name${uuid()}`,
|
name: this.columnsName[2],
|
||||||
dataType: 'VARCHAR',
|
dataType: 'VARCHAR',
|
||||||
dataLength: 100,
|
dataLength: 100,
|
||||||
dataTypeDisplay: 'varchar',
|
dataTypeDisplay: 'varchar',
|
||||||
description: 'Name of the staff member.',
|
description: 'Name of the staff member.',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: `first_name${uuid()}`,
|
name: this.columnsName[3],
|
||||||
dataType: 'STRUCT',
|
dataType: 'STRUCT',
|
||||||
dataLength: 100,
|
dataLength: 100,
|
||||||
dataTypeDisplay:
|
dataTypeDisplay:
|
||||||
@ -85,7 +102,7 @@ export class TableClass extends EntityClass {
|
|||||||
description: 'First name of the staff member.',
|
description: 'First name of the staff member.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `last_name${uuid()}`,
|
name: this.columnsName[4],
|
||||||
dataType: 'ARRAY',
|
dataType: 'ARRAY',
|
||||||
dataLength: 100,
|
dataLength: 100,
|
||||||
dataTypeDisplay: 'array<struct<type:string,provider:array<int>>>',
|
dataTypeDisplay: 'array<struct<type:string,provider:array<int>>>',
|
||||||
@ -93,7 +110,7 @@ export class TableClass extends EntityClass {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `email${uuid()}`,
|
name: this.columnsName[5],
|
||||||
dataType: 'VARCHAR',
|
dataType: 'VARCHAR',
|
||||||
dataLength: 100,
|
dataLength: 100,
|
||||||
dataTypeDisplay: 'varchar',
|
dataTypeDisplay: 'varchar',
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { APIRequestContext } from '@playwright/test';
|
||||||
|
|
||||||
|
const SUGGESTION_DESCRIPTION_DATA = {
|
||||||
|
description: 'this is suggested data description',
|
||||||
|
tagLabels: [
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'Data that can be used to directly or indirectly identify a person.',
|
||||||
|
displayName: 'Personal15',
|
||||||
|
href: 'http://localhost:8585/api/v1/tags/d90759e3-f94e-4e17-8624-9cbe31d347b7',
|
||||||
|
labelType: 'Manual',
|
||||||
|
name: 'Personal',
|
||||||
|
source: 'Classification',
|
||||||
|
state: 'Suggested',
|
||||||
|
style: {
|
||||||
|
color: 'string',
|
||||||
|
iconURL: 'string',
|
||||||
|
},
|
||||||
|
tagFQN: 'PersonalData.Personal15',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'SuggestDescription',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTableSuggestions = async (
|
||||||
|
apiContext: APIRequestContext,
|
||||||
|
entityLink: string
|
||||||
|
) => {
|
||||||
|
const suggestionResponse = await apiContext.post('/api/v1/suggestions', {
|
||||||
|
data: {
|
||||||
|
...SUGGESTION_DESCRIPTION_DATA,
|
||||||
|
entityLink,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return suggestionResponse;
|
||||||
|
};
|
@ -45,7 +45,7 @@ const TableDescription = ({
|
|||||||
entityType === EntityType.TABLE
|
entityType === EntityType.TABLE
|
||||||
? EntityLink.getTableEntityLink(
|
? EntityLink.getTableEntityLink(
|
||||||
entityFqn,
|
entityFqn,
|
||||||
columnData.record?.name ?? ''
|
EntityLink.getTableColumnNameFromColumnFqn(columnData.fqn)
|
||||||
)
|
)
|
||||||
: getEntityFeedLink(entityType, columnData.fqn),
|
: getEntityFeedLink(entityType, columnData.fqn),
|
||||||
[entityType, entityFqn]
|
[entityType, entityFqn]
|
||||||
|
@ -18,11 +18,11 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestion-pending-btn {
|
.ant-btn.suggestion-pending-btn {
|
||||||
background: #ffbe0e0d !important;
|
margin-left: 4px;
|
||||||
border: 1px solid @yellow-2 !important;
|
border-radius: 20px;
|
||||||
color: @yellow-2 !important;
|
padding: 2px 8px;
|
||||||
border-radius: 8px !important;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggested-alert-content {
|
.suggested-alert-content {
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
import { EntityReference } from '../../../generated/entity/type';
|
import { EntityReference } from '../../../generated/entity/type';
|
||||||
|
|
||||||
export interface SuggestionsContextType {
|
export interface SuggestionsContextType {
|
||||||
|
suggestionLimit: number;
|
||||||
selectedUserSuggestions: Suggestion[];
|
selectedUserSuggestions: Suggestion[];
|
||||||
suggestions: Suggestion[];
|
suggestions: Suggestion[];
|
||||||
suggestionsByUser: Map<string, Suggestion[]>;
|
suggestionsByUser: Map<string, Suggestion[]>;
|
||||||
@ -25,7 +26,7 @@ export interface SuggestionsContextType {
|
|||||||
loadingReject: boolean;
|
loadingReject: boolean;
|
||||||
allSuggestionsUsers: EntityReference[];
|
allSuggestionsUsers: EntityReference[];
|
||||||
onUpdateActiveUser: (user?: EntityReference) => void;
|
onUpdateActiveUser: (user?: EntityReference) => void;
|
||||||
fetchSuggestions: (entityFqn: string) => void;
|
fetchSuggestions: () => void;
|
||||||
acceptRejectSuggestion: (
|
acceptRejectSuggestion: (
|
||||||
suggestion: Suggestion,
|
suggestion: Suggestion,
|
||||||
action: SuggestionAction
|
action: SuggestionAction
|
||||||
|
@ -15,7 +15,7 @@ import React from 'react';
|
|||||||
import { SuggestionType } from '../../../generated/entity/feed/suggestion';
|
import { SuggestionType } from '../../../generated/entity/feed/suggestion';
|
||||||
import { mockEntityPermissions } from '../../../pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock';
|
import { mockEntityPermissions } from '../../../pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock';
|
||||||
import {
|
import {
|
||||||
aproveRejectAllSuggestions,
|
approveRejectAllSuggestions,
|
||||||
getSuggestionsList,
|
getSuggestionsList,
|
||||||
updateSuggestionStatus,
|
updateSuggestionStatus,
|
||||||
} from '../../../rest/suggestionsAPI';
|
} from '../../../rest/suggestionsAPI';
|
||||||
@ -45,7 +45,7 @@ jest.mock('../../../hooks/useFqn', () => ({
|
|||||||
|
|
||||||
jest.mock('../../../rest/suggestionsAPI', () => ({
|
jest.mock('../../../rest/suggestionsAPI', () => ({
|
||||||
getSuggestionsList: jest.fn().mockImplementation(() => Promise.resolve()),
|
getSuggestionsList: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
aproveRejectAllSuggestions: jest.fn(),
|
approveRejectAllSuggestions: jest.fn(),
|
||||||
updateSuggestionStatus: jest.fn(),
|
updateSuggestionStatus: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -65,7 +65,10 @@ describe('SuggestionsProvider', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getSuggestionsList).toHaveBeenCalled();
|
expect(getSuggestionsList).toHaveBeenCalledWith({
|
||||||
|
entityFQN: 'mockFQN',
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls approveRejectAllSuggestions when button is clicked', () => {
|
it('calls approveRejectAllSuggestions when button is clicked', () => {
|
||||||
@ -81,7 +84,7 @@ describe('SuggestionsProvider', () => {
|
|||||||
const acceptAllBtn = screen.getByText('Accept All');
|
const acceptAllBtn = screen.getByText('Accept All');
|
||||||
fireEvent.click(acceptAllBtn);
|
fireEvent.click(acceptAllBtn);
|
||||||
|
|
||||||
expect(aproveRejectAllSuggestions).toHaveBeenCalledWith(
|
expect(approveRejectAllSuggestions).toHaveBeenCalledWith(
|
||||||
'1',
|
'1',
|
||||||
'mockFQN',
|
'mockFQN',
|
||||||
SuggestionType.SuggestDescription,
|
SuggestionType.SuggestDescription,
|
||||||
@ -102,7 +105,7 @@ describe('SuggestionsProvider', () => {
|
|||||||
const rejectAll = screen.getByText('Reject All');
|
const rejectAll = screen.getByText('Reject All');
|
||||||
fireEvent.click(rejectAll);
|
fireEvent.click(rejectAll);
|
||||||
|
|
||||||
expect(aproveRejectAllSuggestions).toHaveBeenCalledWith(
|
expect(approveRejectAllSuggestions).toHaveBeenCalledWith(
|
||||||
'1',
|
'1',
|
||||||
'mockFQN',
|
'mockFQN',
|
||||||
SuggestionType.SuggestDescription,
|
SuggestionType.SuggestDescription,
|
||||||
|
@ -33,7 +33,7 @@ import { EntityReference } from '../../../generated/entity/type';
|
|||||||
import { useFqn } from '../../../hooks/useFqn';
|
import { useFqn } from '../../../hooks/useFqn';
|
||||||
import { usePub } from '../../../hooks/usePubSub';
|
import { usePub } from '../../../hooks/usePubSub';
|
||||||
import {
|
import {
|
||||||
aproveRejectAllSuggestions,
|
approveRejectAllSuggestions,
|
||||||
getSuggestionsList,
|
getSuggestionsList,
|
||||||
updateSuggestionStatus,
|
updateSuggestionStatus,
|
||||||
} from '../../../rest/suggestionsAPI';
|
} from '../../../rest/suggestionsAPI';
|
||||||
@ -62,16 +62,19 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
const publish = usePub();
|
const publish = usePub();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [suggestionLimit, setSuggestionLimit] = useState<number>(10);
|
||||||
const refreshEntity = useRef<(suggestion: Suggestion) => void>();
|
const refreshEntity = useRef<(suggestion: Suggestion) => void>();
|
||||||
const { permissions } = usePermissionProvider();
|
const { permissions } = usePermissionProvider();
|
||||||
|
|
||||||
const fetchSuggestions = useCallback(async (entityFQN: string) => {
|
const fetchSuggestions = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const { data } = await getSuggestionsList({
|
const { data, paging } = await getSuggestionsList({
|
||||||
entityFQN,
|
entityFQN: entityFqn,
|
||||||
|
limit: suggestionLimit,
|
||||||
});
|
});
|
||||||
setSuggestions(data);
|
setSuggestions(data);
|
||||||
|
setSuggestionLimit(paging.total);
|
||||||
|
|
||||||
const allUsersData = data.map(
|
const allUsersData = data.map(
|
||||||
(suggestion) => suggestion.createdBy as EntityReference
|
(suggestion) => suggestion.createdBy as EntityReference
|
||||||
@ -100,13 +103,13 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [entityFqn, suggestionLimit]);
|
||||||
|
|
||||||
const acceptRejectSuggestion = useCallback(
|
const acceptRejectSuggestion = useCallback(
|
||||||
async (suggestion: Suggestion, status: SuggestionAction) => {
|
async (suggestion: Suggestion, status: SuggestionAction) => {
|
||||||
try {
|
try {
|
||||||
await updateSuggestionStatus(suggestion, status);
|
await updateSuggestionStatus(suggestion, status);
|
||||||
await fetchSuggestions(entityFqn);
|
await fetchSuggestions();
|
||||||
if (status === SuggestionAction.Accept) {
|
if (status === SuggestionAction.Accept) {
|
||||||
// call component refresh function
|
// call component refresh function
|
||||||
publish('updateDetails', suggestion);
|
publish('updateDetails', suggestion);
|
||||||
@ -115,7 +118,7 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
showErrorToast(err as AxiosError);
|
showErrorToast(err as AxiosError);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[entityFqn, refreshEntity]
|
[fetchSuggestions, refreshEntity]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onUpdateActiveUser = useCallback(
|
const onUpdateActiveUser = useCallback(
|
||||||
@ -137,14 +140,14 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
setLoadingReject(true);
|
setLoadingReject(true);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await aproveRejectAllSuggestions(
|
await approveRejectAllSuggestions(
|
||||||
activeUser?.id ?? '',
|
activeUser?.id ?? '',
|
||||||
entityFqn,
|
entityFqn,
|
||||||
suggestionType,
|
suggestionType,
|
||||||
status
|
status
|
||||||
);
|
);
|
||||||
|
|
||||||
await fetchSuggestions(entityFqn);
|
await fetchSuggestions();
|
||||||
if (status === SuggestionAction.Accept) {
|
if (status === SuggestionAction.Accept) {
|
||||||
selectedUserSuggestions.forEach((suggestion) => {
|
selectedUserSuggestions.forEach((suggestion) => {
|
||||||
publish('updateDetails', suggestion);
|
publish('updateDetails', suggestion);
|
||||||
@ -158,18 +161,19 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
setLoadingReject(false);
|
setLoadingReject(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeUser, entityFqn, selectedUserSuggestions]
|
[activeUser, entityFqn, selectedUserSuggestions, fetchSuggestions]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEmpty(permissions) && !isEmpty(entityFqn)) {
|
if (!isEmpty(permissions) && !isEmpty(entityFqn)) {
|
||||||
fetchSuggestions(entityFqn);
|
fetchSuggestions();
|
||||||
}
|
}
|
||||||
}, [entityFqn, permissions]);
|
}, [entityFqn, permissions]);
|
||||||
|
|
||||||
const suggestionsContextObj = useMemo(() => {
|
const suggestionsContextObj = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
suggestions,
|
suggestions,
|
||||||
|
suggestionLimit,
|
||||||
suggestionsByUser,
|
suggestionsByUser,
|
||||||
selectedUserSuggestions,
|
selectedUserSuggestions,
|
||||||
entityFqn,
|
entityFqn,
|
||||||
@ -184,6 +188,7 @@ const SuggestionsProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
suggestions,
|
suggestions,
|
||||||
|
suggestionLimit,
|
||||||
suggestionsByUser,
|
suggestionsByUser,
|
||||||
selectedUserSuggestions,
|
selectedUserSuggestions,
|
||||||
entityFqn,
|
entityFqn,
|
||||||
|
@ -15,6 +15,9 @@ import React from 'react';
|
|||||||
import { useSuggestionsContext } from '../SuggestionsProvider/SuggestionsProvider';
|
import { useSuggestionsContext } from '../SuggestionsProvider/SuggestionsProvider';
|
||||||
import SuggestionsSlider from './SuggestionsSlider';
|
import SuggestionsSlider from './SuggestionsSlider';
|
||||||
|
|
||||||
|
const acceptRejectAllSuggestions = jest.fn();
|
||||||
|
const fetchSuggestions = jest.fn();
|
||||||
|
|
||||||
jest.mock('../SuggestionsProvider/SuggestionsProvider', () => ({
|
jest.mock('../SuggestionsProvider/SuggestionsProvider', () => ({
|
||||||
useSuggestionsContext: jest.fn(),
|
useSuggestionsContext: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@ -23,15 +26,22 @@ jest.mock('../../common/AvatarCarousel/AvatarCarousel', () => {
|
|||||||
return jest.fn(() => <p>Avatar Carousel</p>);
|
return jest.fn(() => <p>Avatar Carousel</p>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('../SuggestionsProvider/SuggestionsProvider', () => ({
|
||||||
|
useSuggestionsContext: jest.fn().mockImplementation(() => ({
|
||||||
|
suggestions: [{ id: '1' }, { id: '2' }],
|
||||||
|
selectedUserSuggestions: [{ id: '1' }, { id: '2' }],
|
||||||
|
suggestionLimit: 2,
|
||||||
|
acceptRejectAllSuggestions,
|
||||||
|
fetchSuggestions,
|
||||||
|
loadingAccept: false,
|
||||||
|
loadingReject: false,
|
||||||
|
})),
|
||||||
|
__esModule: true,
|
||||||
|
default: 'SuggestionsProvider',
|
||||||
|
}));
|
||||||
|
|
||||||
describe('SuggestionsSlider', () => {
|
describe('SuggestionsSlider', () => {
|
||||||
it('renders buttons when there are selected user suggestions', () => {
|
it('renders buttons when there are selected user suggestions', () => {
|
||||||
(useSuggestionsContext as jest.Mock).mockReturnValue({
|
|
||||||
selectedUserSuggestions: [{ id: '1' }, { id: '2' }],
|
|
||||||
acceptRejectAllSuggestions: jest.fn(),
|
|
||||||
loadingAccept: false,
|
|
||||||
loadingReject: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<SuggestionsSlider />);
|
render(<SuggestionsSlider />);
|
||||||
|
|
||||||
expect(screen.getByTestId('accept-all-suggestions')).toBeInTheDocument();
|
expect(screen.getByTestId('accept-all-suggestions')).toBeInTheDocument();
|
||||||
@ -39,17 +49,37 @@ describe('SuggestionsSlider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls acceptRejectAllSuggestions on button click', () => {
|
it('calls acceptRejectAllSuggestions on button click', () => {
|
||||||
const acceptRejectAllSuggestions = jest.fn();
|
|
||||||
(useSuggestionsContext as jest.Mock).mockReturnValue({
|
|
||||||
selectedUserSuggestions: [{ id: '1' }, { id: '2' }],
|
|
||||||
acceptRejectAllSuggestions,
|
|
||||||
loadingAccept: false,
|
|
||||||
loadingReject: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<SuggestionsSlider />);
|
render(<SuggestionsSlider />);
|
||||||
fireEvent.click(screen.getByTestId('accept-all-suggestions'));
|
fireEvent.click(screen.getByTestId('accept-all-suggestions'));
|
||||||
|
|
||||||
expect(acceptRejectAllSuggestions).toHaveBeenCalled();
|
expect(acceptRejectAllSuggestions).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not renders more suggestion button if limit is match', () => {
|
||||||
|
render(<SuggestionsSlider />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('more-suggestion-button')
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the more suggestion if limit and suggestion doesn't match", () => {
|
||||||
|
(useSuggestionsContext as jest.Mock).mockReturnValueOnce({
|
||||||
|
suggestions: [{ id: '1' }, { id: '2' }],
|
||||||
|
selectedUserSuggestions: [{ id: '1' }, { id: '2' }],
|
||||||
|
suggestionLimit: 20,
|
||||||
|
acceptRejectAllSuggestions,
|
||||||
|
fetchSuggestions,
|
||||||
|
loadingAccept: false,
|
||||||
|
loadingReject: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<SuggestionsSlider />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('more-suggestion-button')).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId('more-suggestion-button'));
|
||||||
|
|
||||||
|
expect(fetchSuggestions).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -22,6 +22,10 @@ import { SuggestionAction } from '../SuggestionsProvider/SuggestionsProvider.int
|
|||||||
|
|
||||||
const SuggestionsSlider = () => {
|
const SuggestionsSlider = () => {
|
||||||
const {
|
const {
|
||||||
|
suggestions,
|
||||||
|
loading,
|
||||||
|
fetchSuggestions,
|
||||||
|
suggestionLimit,
|
||||||
selectedUserSuggestions,
|
selectedUserSuggestions,
|
||||||
acceptRejectAllSuggestions,
|
acceptRejectAllSuggestions,
|
||||||
loadingAccept,
|
loadingAccept,
|
||||||
@ -35,6 +39,18 @@ const SuggestionsSlider = () => {
|
|||||||
{t('label.suggested-description-plural')}
|
{t('label.suggested-description-plural')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<AvatarCarousel />
|
<AvatarCarousel />
|
||||||
|
{suggestions.length !== 0 && suggestions.length !== suggestionLimit && (
|
||||||
|
<Button
|
||||||
|
className="suggestion-pending-btn"
|
||||||
|
data-testid="more-suggestion-button"
|
||||||
|
loading={loading}
|
||||||
|
type="primary"
|
||||||
|
onClick={fetchSuggestions}>
|
||||||
|
{t('label.plus-count-more', {
|
||||||
|
count: suggestionLimit - 10, // 10 is the default limit, and only show count of pending suggestions
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{selectedUserSuggestions.length > 0 && (
|
{selectedUserSuggestions.length > 0 && (
|
||||||
<Space className="slider-btn-container m-l-xs">
|
<Space className="slider-btn-container m-l-xs">
|
||||||
<Button
|
<Button
|
||||||
|
@ -33,6 +33,10 @@
|
|||||||
}
|
}
|
||||||
.slick-list {
|
.slick-list {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
|
|
||||||
|
.slick-track {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.ant-badge-count {
|
.ant-badge-count {
|
||||||
right: 4px;
|
right: 4px;
|
||||||
|
@ -24,6 +24,7 @@ const BASE_URL = '/suggestions';
|
|||||||
|
|
||||||
export type ListSuggestionsParams = ListParams & {
|
export type ListSuggestionsParams = ListParams & {
|
||||||
entityFQN?: string;
|
entityFQN?: string;
|
||||||
|
limit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSuggestionsList = async (params?: ListSuggestionsParams) => {
|
export const getSuggestionsList = async (params?: ListSuggestionsParams) => {
|
||||||
@ -43,7 +44,7 @@ export const updateSuggestionStatus = (
|
|||||||
return APIClient.put(url, {});
|
return APIClient.put(url, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const aproveRejectAllSuggestions = (
|
export const approveRejectAllSuggestions = (
|
||||||
userId: string,
|
userId: string,
|
||||||
entityFQN: string,
|
entityFQN: string,
|
||||||
suggestionType: SuggestionType,
|
suggestionType: SuggestionType,
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
@yellow-6: #c18100;
|
@yellow-6: #c18100;
|
||||||
@yellow-7: #e7b85d;
|
@yellow-7: #e7b85d;
|
||||||
@yellow-8: #ffbe0e08;
|
@yellow-8: #ffbe0e08;
|
||||||
|
@yellow-9: #ffbe0e0d;
|
||||||
@error-light-color: #ff9a8e40;
|
@error-light-color: #ff9a8e40;
|
||||||
@failed-color: #cb2431;
|
@failed-color: #cb2431;
|
||||||
@red-1: #cb2531;
|
@red-1: #cb2531;
|
||||||
|
@ -13,8 +13,11 @@
|
|||||||
import antlr4 from 'antlr4';
|
import antlr4 from 'antlr4';
|
||||||
import { ParseTreeWalker } from 'antlr4/src/antlr4/tree';
|
import { ParseTreeWalker } from 'antlr4/src/antlr4/tree';
|
||||||
import EntityLinkSplitListener from '../antlr/EntityLinkSplitListener';
|
import EntityLinkSplitListener from '../antlr/EntityLinkSplitListener';
|
||||||
|
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
|
||||||
|
import { FqnPart } from '../enums/entity.enum';
|
||||||
import EntityLinkLexer from '../generated/antlr/EntityLinkLexer';
|
import EntityLinkLexer from '../generated/antlr/EntityLinkLexer';
|
||||||
import EntityLinkParser from '../generated/antlr/EntityLinkParser';
|
import EntityLinkParser from '../generated/antlr/EntityLinkParser';
|
||||||
|
import { getPartialNameFromTableFQN } from './CommonUtils';
|
||||||
import { ENTITY_LINK_SEPARATOR } from './EntityUtils';
|
import { ENTITY_LINK_SEPARATOR } from './EntityUtils';
|
||||||
|
|
||||||
export default class EntityLink {
|
export default class EntityLink {
|
||||||
@ -86,6 +89,21 @@ export default class EntityLink {
|
|||||||
return this.split(entityLink)[3];
|
return this.split(entityLink)[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param string columnFqn
|
||||||
|
* @returns column name for table entity
|
||||||
|
*/
|
||||||
|
static getTableColumnNameFromColumnFqn(columnFqn: string) {
|
||||||
|
const columnName = getPartialNameFromTableFQN(columnFqn, [
|
||||||
|
FqnPart.NestedColumn,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return columnName.includes(FQN_SEPARATOR_CHAR)
|
||||||
|
? `"${columnName}"`
|
||||||
|
: columnName;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param string entityLink
|
* @param string entityLink
|
||||||
|
Loading…
x
Reference in New Issue
Block a user