({
}));
jest.mock('../../../../../utils/CommonUtils', () => ({
- getNonDeletedTeams: jest.fn().mockReturnValue([]),
+ getNonDeletedTeams: jest.fn().mockImplementation((data) => data),
}));
jest.mock('../../../../common/InlineEdit/InlineEdit.component', () => {
- return jest.fn().mockImplementation(({ onSave }) => (
+ return jest.fn().mockImplementation(({ children, onCancel, onSave }) => (
InlineEdit
+ {children}
+
));
});
@@ -47,7 +52,30 @@ jest.mock('../../../../common/Chip/Chip.component', () => {
});
jest.mock('../../../Team/TeamsSelectable/TeamsSelectable', () => {
- return jest.fn().mockReturnValue(TeamsSelectable
);
+ return jest
+ .fn()
+ .mockImplementation(({ selectedTeams, onSelectionChange }) => (
+
+
TeamsSelectable
+
+ {selectedTeams.map(
+ (item: EntityReference) => item.fullyQualifiedName
+ )}
+
+
+ onSelectionChange([
+ {
+ id: '37a00e0b-383c-4451-b63f-0bad4c745abc',
+ name: 'admin',
+ type: 'team',
+ },
+ ])
+ }
+ />
+
+ ));
});
describe('Test User Profile Teams Component', () => {
@@ -67,18 +95,26 @@ describe('Test User Profile Teams Component', () => {
expect(await screen.findAllByText('Chip')).toHaveLength(1);
});
- it('should render teams select input on edit click', async () => {
+ it('should maintain initial state if edit is close without save', async () => {
render();
- expect(screen.getByTestId('user-team-card-container')).toBeInTheDocument();
+ fireEvent.click(screen.getByTestId('edit-teams-button'));
- const editButton = screen.getByTestId('edit-teams-button');
+ const selectInput = screen.getByTestId('select-user-teams');
+
+ act(() => {
+ fireEvent.change(selectInput, {
+ target: {
+ value: 'test',
+ },
+ });
+ });
- expect(editButton).toBeInTheDocument();
+ fireEvent.click(screen.getByTestId('cancel'));
- fireEvent.click(editButton);
+ fireEvent.click(screen.getByTestId('edit-teams-button'));
- expect(screen.getByText('InlineEdit')).toBeInTheDocument();
+ expect(screen.getByText('Organization')).toBeInTheDocument();
});
it('should call updateUserDetails on click save', async () => {
@@ -95,7 +131,14 @@ describe('Test User Profile Teams Component', () => {
});
expect(mockPropsData.updateUserDetails).toHaveBeenCalledWith(
- { teams: [] },
+ {
+ teams: [
+ {
+ id: '9e8b7464-3f3e-4071-af05-19be142d75db',
+ type: 'team',
+ },
+ ],
+ },
'teams'
);
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.component.tsx
index 646d5f314b75..318a9f588085 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.component.tsx
@@ -13,17 +13,21 @@
import { Col, Popover, Row, Space, Tag, Typography } from 'antd';
import { isEmpty } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
+import { Link } from 'react-router-dom';
import {
NO_DATA_PLACEHOLDER,
USER_DATA_SIZE,
} from '../../../constants/constants';
import { EntityReference } from '../../../generated/entity/type';
+import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
import { getEntityName } from '../../../utils/EntityUtils';
import { ChipProps } from './Chip.interface';
+import './chip.less';
const Chip = ({
data,
icon,
+ entityType,
noDataPlaceholder,
showNoDataPlaceholder = true,
}: ChipProps) => {
@@ -35,14 +39,19 @@ const Chip = ({
);
const getChipElement = (item: EntityReference) => (
-
- {icon}
-
- {getEntityName(item)}
-
+
+
+ {icon}
+
+ {getEntityName(item)}
+
+
);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.interface.ts
index 21b4af366f7a..819a6d03b800 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.interface.ts
@@ -10,10 +10,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import { EntityType } from '../../../enums/entity.enum';
import { EntityReference } from '../../../generated/entity/type';
export interface ChipProps {
data: EntityReference[];
+ entityType: EntityType;
icon?: React.ReactElement;
noDataPlaceholder?: string;
showNoDataPlaceholder?: boolean;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.test.tsx
new file mode 100644
index 000000000000..812df3f4f2f7
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.test.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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 { fireEvent, render, screen } from '@testing-library/react';
+import React from 'react';
+import {
+ NO_DATA_PLACEHOLDER,
+ USER_DATA_SIZE,
+} from '../../../constants/constants';
+import { EntityType } from '../../../enums/entity.enum';
+import { MOCK_USER_ROLE } from '../../../mocks/User.mock';
+import Chip from './Chip.component';
+
+const mockLinkButton = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+ Link: jest.fn().mockImplementation(({ children, ...rest }) => (
+
+ {children}
+
+ )),
+}));
+
+jest.mock('../../../utils/EntityUtils', () => ({
+ getEntityName: jest.fn().mockReturnValue('getEntityName'),
+}));
+
+jest.mock('../../../utils/EntityUtilClassBase', () => ({
+ getEntityLink: jest.fn(),
+}));
+
+const mockProps = {
+ data: [],
+ entityType: EntityType.ROLE,
+};
+
+describe('Test Chip Component', () => {
+ it('should renders errorPlaceholder in case of no data', () => {
+ render();
+
+ expect(screen.getByText(NO_DATA_PLACEHOLDER)).toBeInTheDocument();
+ });
+
+ it('should renders noDataPlaceholder if provided', () => {
+ const placeholder = 'this is custom placeholder';
+
+ render();
+
+ expect(screen.getByText(placeholder)).toBeInTheDocument();
+ });
+
+ it('should renders tag chips', () => {
+ render(
+
+ );
+
+ expect(screen.getAllByTestId('tag-chip')).toHaveLength(5);
+ expect(screen.getAllByText('getEntityName')).toHaveLength(5);
+ });
+
+ it('should renders more chip button if data is more than the size', () => {
+ render();
+
+ expect(screen.getByTestId('plus-more-count')).toBeInTheDocument();
+ expect(screen.getByText('+3 more')).toBeInTheDocument();
+ });
+
+ it('should redirect the page when click on tag chip', () => {
+ render();
+
+ const tagChip = screen.getByTestId('ApplicationBotRole-link');
+
+ fireEvent.click(tagChip);
+
+ expect(mockLinkButton).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/chip.less b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/chip.less
new file mode 100644
index 000000000000..fe5622b66ef8
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/chip.less
@@ -0,0 +1,23 @@
+/*
+ * 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 (reference) url('../../../styles/variables.less');
+
+.chip-tag-link {
+ display: flex;
+ color: @black;
+ gap: 4px;
+
+ &:hover {
+ color: @black;
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/User.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/User.mock.ts
index a70288c2a26b..064f934405a5 100644
--- a/openmetadata-ui/src/main/resources/ui/src/mocks/User.mock.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/mocks/User.mock.ts
@@ -150,3 +150,168 @@ export const USER_TEAMS = [
href: 'http://localhost:8585/api/v1/teams/9e8b7464-3f3e-4071-af05-19be142d75bc',
},
];
+
+export const MOCK_USER_ROLE = [
+ {
+ id: '37a00e0b-383c-4451-b63f-0bad4c745abc',
+ type: 'role',
+ name: 'ApplicationBotRole',
+ fullyQualifiedName: 'ApplicationBotRole',
+ description: 'Role corresponding to a Application bot.',
+ displayName: 'Application bot role',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/roles/37a00e0b-383c-4451-b63f-0bad4c745abc',
+ },
+ {
+ id: 'afc5583c-e268-4f6c-a638-a876d04ebaa1',
+ type: 'role',
+ name: 'DataConsumer',
+ fullyQualifiedName: 'DataConsumer',
+ description:
+ 'Users with Data Consumer role use different data assets for their day to day work.',
+ displayName: 'Data Consumer',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/roles/afc5583c-e268-4f6c-a638-a876d04ebaa1',
+ },
+ {
+ id: '013746ec-2159-496e-88f7-f7175a2af919',
+ type: 'role',
+ name: 'DataQualityBotRole',
+ fullyQualifiedName: 'DataQualityBotRole',
+ description: 'Role corresponding to a Data quality bot.',
+ displayName: 'Data quality Bot role',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/roles/013746ec-2159-496e-88f7-f7175a2af919',
+ },
+ {
+ id: 'dd72bae6-1835-4ba9-9532-aaa4b648d3e8',
+ type: 'role',
+ name: 'DataSteward',
+ fullyQualifiedName: 'DataSteward',
+ description:
+ 'Users with Data Steward role are responsible for ensuring correctness of metadata for data assets, thereby facilitating data governance principles within the organization.',
+ displayName: 'Data Steward',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/roles/dd72bae6-1835-4ba9-9532-aaa4b648d3e8',
+ },
+ {
+ id: '6b007040-1378-4de9-a8b0-f922fc9f4e25',
+ type: 'role',
+ name: 'IngestionBotRole',
+ fullyQualifiedName: 'IngestionBotRole',
+ description: 'Role corresponding to a Ingestion bot.',
+ displayName: 'Ingestion bot role',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/roles/6b007040-1378-4de9-a8b0-f922fc9f4e25',
+ },
+ {
+ id: '7f8de4ae-8b08-431c-9911-8a355aa2976e',
+ type: 'role',
+ name: 'ProfilerBotRole',
+ fullyQualifiedName: 'ProfilerBotRole',
+ description: 'Role corresponding to a Profiler bot.',
+ displayName: 'Profiler bot role',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/roles/7f8de4ae-8b08-431c-9911-8a355aa2976e',
+ },
+ {
+ id: '7082d70a-ddb2-42db-b639-3ec4c7884c52',
+ type: 'role',
+ name: 'QualityBotRole',
+ fullyQualifiedName: 'QualityBotRole',
+ description: 'Role corresponding to a Quality bot.',
+ displayName: 'Quality bot role',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/roles/7082d70a-ddb2-42db-b639-3ec4c7884c52',
+ },
+ {
+ id: 'admin',
+ type: 'role',
+ name: 'Admin',
+ },
+];
+
+export const UPDATED_USER_DATA = {
+ changeDescription: {
+ fieldsAdded: [],
+ fieldsDeleted: [],
+ fieldsUpdated: [],
+ previousVersion: 3.2,
+ },
+ defaultPersona: {
+ description: 'Person-04',
+ displayName: 'Person-04',
+ fullyQualifiedName: 'Person-04',
+ href: 'http://localhost:8585/api/v1/personas/0430976d-092a-46c9-90a8-61c6091a6f38',
+ id: '0430976d-092a-46c9-90a8-61c6091a6f38',
+ name: 'Person-04',
+ type: 'persona',
+ },
+ deleted: false,
+ description: '',
+ displayName: '',
+ domain: {
+ description: 'description',
+ fullyQualifiedName: 'Engineering',
+ href: 'http://localhost:8585/api/v1/domains/303ca53b-5050-4caa-9c4e-d4fdada76a53',
+ id: '303ca53b-5050-4caa-9c4e-d4fdada76a53',
+ inherited: true,
+ name: 'Engineering',
+ type: 'domain',
+ },
+ email: 'admin@openmetadata.org',
+ fullyQualifiedName: 'admin',
+ href: 'http://localhost:8585/api/v1/users/7f196a28-c4fa-4579-b420-f828985e7861',
+ id: '7f196a28-c4fa-4579-b420-f828985e7861',
+ inheritedRoles: [
+ {
+ deleted: false,
+ description:
+ 'Users with Data Consumer role use different data assets for their day to day work.',
+ displayName: 'Data Consumer',
+ fullyQualifiedName: 'DataConsumer',
+ href: 'http://localhost:8585/api/v1/roles/ed94fd7c-0974-4b87-9295-02b36c4c6bcd',
+ id: 'ed94fd7c-0974-4b87-9295-02b36c4c6bcd',
+ name: 'DataConsumer',
+ type: 'role',
+ },
+ ],
+ isAdmin: false,
+ isBot: false,
+ isEmailVerified: true,
+ name: 'admin',
+ personas: [
+ {
+ description: 'Person-04',
+ displayName: 'Person-04',
+ fullyQualifiedName: 'Person-04',
+ href: 'http://localhost:8585/api/v1/personas/0430976d-092a-46c9-90a8-61c6091a6f38',
+ id: '0430976d-092a-46c9-90a8-61c6091a6f38',
+ name: 'Person-04',
+ type: 'persona',
+ },
+ ],
+ roles: [
+ {
+ id: '7f8de4ae-8b08-431c-9911-8a355aa2976e',
+ name: 'ProfilerBotRole',
+ type: 'role',
+ },
+ ],
+ teams: [
+ {
+ deleted: false,
+ description:
+ 'Organization under which all the other team hierarchy is created',
+ displayName: 'Organization',
+ fullyQualifiedName: 'Organization',
+ href: 'http://localhost:8585/api/v1/teams/9e8b7464-3f3e-4071-af05-19be142d75db',
+ id: '9e8b7464-3f3e-4071-af05-19be142d75db',
+ name: 'Organization',
+ type: 'team',
+ },
+ ],
+ updatedAt: 1698655259882,
+ updatedBy: 'admin',
+ version: 3.3,
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx
index 1c6561caf739..393710ab512f 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx
@@ -111,11 +111,21 @@ const UserPage = () => {
try {
const response = await updateUserDetail(userData.id, jsonPatch);
if (response) {
+ let updatedKeyData;
+
+ if (key === 'roles') {
+ updatedKeyData = {
+ roles: response.roles,
+ isAdmin: response.isAdmin,
+ };
+ } else {
+ updatedKeyData = { [key]: response[key] };
+ }
const newCurrentUserData = {
...currentUser,
- [key]: response[key],
+ ...updatedKeyData,
};
- const newUserData = { ...userData, [key]: response[key] };
+ const newUserData = { ...userData, ...updatedKeyData };
if (key === 'defaultPersona') {
if (isUndefined(response.defaultPersona)) {
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.test.tsx
index 436d9a71b6c5..d322ed3418d1 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.test.tsx
@@ -21,8 +21,9 @@ import {
} from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
+import Users from '../../components/Settings/Users/Users.component';
import { useApplicationStore } from '../../hooks/useApplicationStore';
-import { USER_DATA } from '../../mocks/User.mock';
+import { UPDATED_USER_DATA, USER_DATA } from '../../mocks/User.mock';
import { getUserByName, updateUserDetail } from '../../rest/userAPI';
import UserPage from './UserPage.component';
@@ -145,6 +146,45 @@ describe('Test the User Page', () => {
expect(mockUpdateCurrentUser).toHaveBeenCalled();
});
+ it('should update user isAdmin details if changes along with user', async () => {
+ (Users as jest.Mock).mockImplementationOnce(({ updateUserDetails }) => (
+
+
+
+ ));
+
+ (updateUserDetail as jest.Mock).mockImplementationOnce(() =>
+ Promise.resolve(UPDATED_USER_DATA)
+ );
+
+ await act(async () => {
+ render(, { wrapper: MemoryRouter });
+ });
+
+ await act(async () => {
+ fireEvent.click(screen.getByText('UserComponentSaveButton'));
+ });
+
+ expect(mockUpdateCurrentUser).toHaveBeenCalledWith(UPDATED_USER_DATA);
+ });
+
it('Should not call updateCurrentUser if user is not currentUser logged in', async () => {
(useApplicationStore as unknown as jest.Mock).mockImplementation(() => ({
currentUser: { ...USER_DATA, id: '123' },