diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component.tsx
index bf19eeacc68..ae6077214a0 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component.tsx
@@ -26,6 +26,7 @@ import {
} from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
+import { LIGHT_GREEN_COLOR } from '../../../constants/constants';
import { Transi18next } from '../../../utils/CommonUtils';
import { getRelativeTime } from '../../../utils/date-time/DateTimeUtils';
import { getEntityName } from '../../../utils/EntityUtils';
@@ -50,7 +51,7 @@ const AppInstallVerifyCard = ({
-
+
)}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/UserProfileIcon/UserProfileIcon.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/UserProfileIcon/UserProfileIcon.component.tsx
index d450b838322..89a5eba0917 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Users/UserProfileIcon/UserProfileIcon.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/UserProfileIcon/UserProfileIcon.component.tsx
@@ -11,7 +11,7 @@
* limitations under the License.
*/
import { CheckOutlined } from '@ant-design/icons';
-import { Dropdown, Tooltip, Typography } from 'antd';
+import { Dropdown, Space, Tooltip, Typography } from 'antd';
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { isEmpty } from 'lodash';
import React, {
@@ -27,6 +27,7 @@ import { ReactComponent as DropDownIcon } from '../../../assets/svg/DropDown.svg
import {
getTeamAndUserDetailsPath,
getUserPath,
+ LIGHT_GREEN_COLOR,
NO_DATA_PLACEHOLDER,
TERM_ADMIN,
TERM_USER,
@@ -111,6 +112,8 @@ export const UserProfileIcon = () => {
useEffect(() => {
if (profilePicture) {
setIsImgUrlValid(true);
+ } else {
+ setIsImgUrlValid(false);
}
}, [profilePicture]);
@@ -133,12 +136,19 @@ export const UserProfileIcon = () => {
const personaLabelRenderer = useCallback(
(item: EntityReference) => (
- handleSelectedPersonaChange(item)}>
+ handleSelectedPersonaChange(item)}>
{getEntityName(item)}{' '}
{selectedPersona?.id === item.id && (
-
+
)}
-
+
),
[handleSelectedPersonaChange, selectedPersona]
);
@@ -317,7 +327,8 @@ export const UserProfileIcon = () => {
{isImgUrlValid ? (
{
{isEmpty(selectedPersona)
? t('label.default')
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/UserProfileIcon/UserProfileIcon.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/UserProfileIcon/UserProfileIcon.test.tsx
new file mode 100644
index 00000000000..c8e5e278046
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/UserProfileIcon/UserProfileIcon.test.tsx
@@ -0,0 +1,173 @@
+/*
+ * 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 { act, fireEvent, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import { getImageWithResolutionAndFallback } from '../../../utils/ProfilerUtils';
+import { useApplicationConfigContext } from '../../ApplicationConfigProvider/ApplicationConfigProvider';
+import { useAuthContext } from '../../Auth/AuthProviders/AuthProvider';
+import { mockPersonaData, mockUserData } from '../mocks/User.mocks';
+import { UserProfileIcon } from './UserProfileIcon.component';
+
+const mockLogout = jest.fn();
+const mockUpdateSelectedPersona = jest.fn();
+
+jest.mock('../../ApplicationConfigProvider/ApplicationConfigProvider', () => ({
+ useApplicationConfigContext: jest.fn().mockImplementation(() => ({
+ selectedPersona: {},
+ updateSelectedPersona: mockUpdateSelectedPersona,
+ })),
+}));
+
+jest.mock('../../../utils/EntityUtils', () => ({
+ getEntityName: jest.fn().mockReturnValue('Test User'),
+}));
+
+jest.mock('../../../utils/ProfilerUtils', () => ({
+ getImageWithResolutionAndFallback: jest
+ .fn()
+ .mockImplementation(() => 'valid-image-url'),
+ ImageQuality: jest.fn().mockReturnValue('6x'),
+}));
+
+jest.mock('../../common/AvatarComponent/Avatar', () =>
+ jest.fn().mockReturnValue(Avatar
)
+);
+
+jest.mock('react-router-dom', () => ({
+ Link: jest
+ .fn()
+ .mockImplementation(({ children }: { children: React.ReactNode }) => (
+ {children}
+ )),
+}));
+
+jest.mock('../../Auth/AuthProviders/AuthProvider', () => ({
+ useAuthContext: jest.fn(() => ({
+ currentUser: mockUserData,
+ })),
+ onLogoutHandler: mockLogout,
+}));
+
+describe('UserProfileIcon', () => {
+ it('should render User Profile Icon', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('dropdown-profile')).toBeInTheDocument();
+ });
+
+ it('should display the user name', () => {
+ const { getByText } = render();
+
+ expect(getByText('Test User')).toBeInTheDocument();
+ });
+
+ it('should display default in case of no persona is selected', () => {
+ const { getByText } = render();
+
+ expect(getByText('label.default')).toBeInTheDocument();
+ });
+
+ it('should display image if profile pic is valid', () => {
+ const { getByTestId } = render();
+
+ expect(getByTestId('app-bar-user-profile-pic')).toBeInTheDocument();
+ });
+
+ it('should not display profile pic if image url is invalid', () => {
+ (getImageWithResolutionAndFallback as jest.Mock).mockImplementation(
+ () => undefined
+ );
+ const { queryByTestId, getByText } = render();
+
+ expect(queryByTestId('app-bar-user-profile-pic')).not.toBeInTheDocument();
+ expect(getByText('Avatar')).toBeInTheDocument();
+ });
+
+ it('should display the user team', () => {
+ (useApplicationConfigContext as jest.Mock).mockImplementation(() => ({
+ selectedPersona: {
+ id: '3362fe18-05ad-4457-9632-84f22887dda6',
+ type: 'team',
+ },
+ updateSelectedPersona: jest.fn(),
+ }));
+ const { getByTestId } = render();
+
+ expect(getByTestId('default-persona')).toHaveTextContent('Test User');
+ });
+
+ it('should show empty placeholder when no teams data', async () => {
+ (useAuthContext as jest.Mock).mockImplementation(() => ({
+ currentUser: { ...mockUserData, teams: [] },
+ onLogoutHandler: mockLogout,
+ }));
+ const teamLabels = screen.queryAllByText('label.team-plural');
+
+ teamLabels.forEach((label) => {
+ expect(label).toHaveTextContent('--');
+ });
+ });
+
+ it('should show checked if selected persona is true', async () => {
+ (useAuthContext as jest.Mock).mockImplementation(() => ({
+ currentUser: {
+ ...mockUserData,
+ personas: mockPersonaData,
+ },
+ onLogoutHandler: mockLogout,
+ }));
+ (useApplicationConfigContext as jest.Mock).mockImplementation(() => ({
+ selectedPersona: {
+ id: '0430976d-092a-46c9-90a8-61c6091a6f38',
+ type: 'persona',
+ },
+ updateSelectedPersona: jest.fn(),
+ }));
+ const { getByTestId } = render();
+ await act(async () => {
+ userEvent.click(getByTestId('dropdown-profile'));
+ });
+ await act(async () => {
+ fireEvent.click(getByTestId('persona-label'));
+ });
+
+ expect(getByTestId('check-outlined')).toBeInTheDocument();
+ });
+
+ it('should not show checked if selected persona is true', async () => {
+ (useAuthContext as jest.Mock).mockImplementation(() => ({
+ currentUser: {
+ ...mockUserData,
+ personas: mockPersonaData,
+ },
+ onLogoutHandler: mockLogout,
+ }));
+ (useApplicationConfigContext as jest.Mock).mockImplementation(() => ({
+ selectedPersona: {
+ id: 'test',
+ type: 'persona',
+ },
+ updateSelectedPersona: jest.fn(),
+ }));
+ const { getByTestId, queryByTestId } = render();
+ await act(async () => {
+ userEvent.click(getByTestId('dropdown-profile'));
+ });
+ await act(async () => {
+ fireEvent.click(getByTestId('persona-label'));
+ });
+
+ expect(queryByTestId('check-outlined')).not.toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersTab/UsersTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersTab/UsersTab.test.tsx
new file mode 100644
index 00000000000..a1a015a459d
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersTab/UsersTab.test.tsx
@@ -0,0 +1,124 @@
+/*
+ * 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 { act, fireEvent, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import React from 'react';
+import { MemoryRouter } from 'react-router-dom';
+import { getUserById } from '../../../rest/userAPI';
+import { mockUsersTabData } from '../mocks/User.mocks';
+import { UsersTab } from './UsersTabs.component';
+
+jest.mock('../../../rest/userAPI', () => ({
+ getUserById: jest
+ .fn()
+ .mockImplementation(() => Promise.resolve(mockUsersTabData)),
+}));
+
+jest.mock('../../common/PopOverCard/UserPopOverCard', () =>
+ jest.fn().mockReturnValue('Aaron Johnson')
+);
+jest.mock('react-router-dom', () => ({
+ Link: jest
+ .fn()
+ .mockImplementation(({ children }: { children: React.ReactNode }) => (
+ {children}
+ )),
+ useHistory: jest.fn(),
+}));
+
+const mockUsers = [
+ {
+ deleted: false,
+ displayName: 'Aaron Johnson',
+ fullyQualifiedName: 'aaron_johnson0',
+ href: 'http://localhost:8585/api/v1/users/f281e7fd-5fd3-4279-8a2d-ade80febd743',
+ id: 'f281e7fd-5fd3-4279-8a2d-ade80febd743',
+ name: 'aaron_johnson0',
+ type: 'user',
+ },
+];
+
+const mockOnRemoveUser = jest.fn();
+
+describe('UsersTab', () => {
+ it('should renders Users Tab', async () => {
+ await act(async () => {
+ render(, {
+ wrapper: MemoryRouter,
+ });
+ });
+
+ expect(await screen.findByText('label.username')).toBeInTheDocument();
+ expect(await screen.findByText('label.team-plural')).toBeInTheDocument();
+ expect(await screen.findByText('label.role-plural')).toBeInTheDocument();
+ expect(await screen.findByText('label.action-plural')).toBeInTheDocument();
+ });
+
+ it('should display the user details', async () => {
+ await act(async () => {
+ render(, {
+ wrapper: MemoryRouter,
+ });
+ });
+
+ expect(await screen.findByText('Aaron Johnson')).toBeInTheDocument();
+ expect(await screen.findByText('Sales')).toBeInTheDocument();
+ expect(await screen.findByText('Data Steward')).toBeInTheDocument();
+ });
+
+ it('should render empty placeholder if no data', async () => {
+ (getUserById as jest.Mock).mockImplementation(() => Promise.resolve([])),
+ await act(async () => {
+ render(, {
+ wrapper: MemoryRouter,
+ });
+ });
+
+ expect(
+ await screen.findByTestId('assign-error-placeholder-label.user')
+ ).toBeInTheDocument();
+ });
+
+ it('should display the remove confirmation modal when remove button is clicked', async () => {
+ await act(async () => {
+ render(, {
+ wrapper: MemoryRouter,
+ });
+ });
+ await act(async () => {
+ fireEvent.click(screen.getByTestId('remove-user-btn'));
+ });
+
+ expect(
+ await screen.getByTestId('remove-confirmation-modal')
+ ).toBeInTheDocument();
+ });
+
+ it('should close the remove confirmation modal when cancel button is clicked', async () => {
+ await act(async () => {
+ render(, {
+ wrapper: MemoryRouter,
+ });
+ });
+ await act(async () => {
+ userEvent.click(screen.getByTestId('remove-user-btn'));
+ });
+ await act(async () => {
+ userEvent.click(screen.getByText('label.cancel'));
+ });
+
+ expect(
+ screen.queryByTestId('remove-confirmation-modal')
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersTab/UsersTabs.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersTab/UsersTabs.component.tsx
index b0d26a980db..10f0022bae7 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersTab/UsersTabs.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersTab/UsersTabs.component.tsx
@@ -134,20 +134,22 @@ export const UsersTab = ({ users, onRemoveUser }: UsersTabProps) => {
rowKey="fullyQualifiedName"
size="small"
/>
-
- {t('message.are-you-sure-want-to-text', {
- text: t('label.remove-entity-lowercase', {
- entity: removeUserDetails?.user.name,
- }),
- })}
-
+ {Boolean(removeUserDetails?.state) && (
+
+ {t('message.are-you-sure-want-to-text', {
+ text: t('label.remove-entity-lowercase', {
+ entity: removeUserDetails?.user.name,
+ }),
+ })}
+
+ )}
>
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/mocks/User.mocks.ts b/openmetadata-ui/src/main/resources/ui/src/components/Users/mocks/User.mocks.ts
index 7832c71855e..9b98b85c312 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Users/mocks/User.mocks.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/mocks/User.mocks.ts
@@ -208,6 +208,71 @@ export const mockTeamsData = {
],
paging: { total: 7 },
};
+export const mockUsersTabData = {
+ id: 'f281e7fd-5fd3-4279-8a2d-ade80febd743',
+ name: 'aaron_johnson0',
+ fullyQualifiedName: 'aaron_johnson0',
+ displayName: 'Aaron Johnson',
+ version: 1.5,
+ updatedAt: 1707198736848,
+ updatedBy: 'admin',
+ email: 'aaron_johnson0@gmail.com',
+ href: 'http://localhost:8585/api/v1/users/f281e7fd-5fd3-4279-8a2d-ade80febd743',
+ isAdmin: false,
+ teams: [
+ {
+ id: '35c03c5c-5160-41af-b08d-ef2b2b9e6adf',
+ type: 'team',
+ name: 'Sales',
+ fullyQualifiedName: 'Sales',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/teams/35c03c5c-5160-41af-b08d-ef2b2b9e6adf',
+ },
+ ],
+ changeDescription: {
+ fieldsAdded: [
+ {
+ name: 'teams',
+ newValue:
+ '[{"id":"35c03c5c-5160-41af-b08d-ef2b2b9e6adf","type":"team","name":"Sales","fullyQualifiedName":"Sales","deleted":false}]',
+ },
+ ],
+ fieldsUpdated: [],
+ fieldsDeleted: [
+ {
+ name: 'teams',
+ oldValue: '[{"id":"f39f326a-81a1-42ca-976]',
+ },
+ ],
+ previousVersion: 1.4,
+ },
+ deleted: false,
+ roles: [
+ {
+ id: 'e4b20aef-c6c4-4416-aaae-f60185c7cac0',
+ type: 'role',
+ name: 'DataSteward',
+ fullyQualifiedName: 'DataSteward',
+ description: 'Users with Data Steward',
+ displayName: 'Data Steward',
+ deleted: false,
+ href: 'http://localhost:8585/api/v1/roles/e4b20aef-c6c4-4416-aaae-f60185c7cac0',
+ },
+ ],
+ inheritedRoles: [
+ {
+ id: '5f1445a7-c299-4dde-8c5b-704c6cd68ee6',
+ 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/5f1445a7-c299-4dde-8c5b-704c6cd68ee6',
+ },
+ ],
+};
export const mockUserRole = {
data: [
@@ -1039,3 +1104,14 @@ export const mockAccessData = {
tokenType: TokenType.PersonalAccessToken,
userId: '445291f4d62c1bae',
};
+export const mockPersonaData = [
+ {
+ 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',
+ },
+];
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Widgets/FeedsWidget/FeedsWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Widgets/FeedsWidget/FeedsWidget.test.tsx
index 168332b7575..32dbec765c6 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Widgets/FeedsWidget/FeedsWidget.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Widgets/FeedsWidget/FeedsWidget.test.tsx
@@ -136,7 +136,7 @@ describe('FeedsWidget', () => {
const { getAllByRole } = render();
const tabs = getAllByRole('tab');
const selectedTab = tabs[index];
- selectedTab.click();
+ fireEvent.click(selectedTab);
expect(selectedTab.getAttribute('aria-selected')).toBe('true');
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
index d62e7101679..3238156ece3 100644
--- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
@@ -42,6 +42,7 @@ export const GREEN_COLOR_OPACITY_30 = '#28A74530';
export const BORDER_COLOR = '#0000001a';
export const BLACK_COLOR = '#000000';
export const WHITE_COLOR = '#ffffff';
+export const LIGHT_GREEN_COLOR = '#4CAF50';
export const DEFAULT_CHART_OPACITY = 1;
export const HOVER_CHART_OPACITY = 0.3;
diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/app.less b/openmetadata-ui/src/main/resources/ui/src/styles/app.less
index 18bad6b8967..29a88fbf721 100644
--- a/openmetadata-ui/src/main/resources/ui/src/styles/app.less
+++ b/openmetadata-ui/src/main/resources/ui/src/styles/app.less
@@ -700,7 +700,7 @@ a[href].link-text-grey,
padding-right: 0.25rem;
}
-.app-bar-user-avatar {
+.app-bar-user-profile-pic {
display: inline-block;
height: 36px;
width: 36px;