mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-03 12:08:31 +00:00
feat(ui): revamp user profile page (#13744)
* revamp user profile page * minor fixes * unit test fixes * added unit test for user components * added unit test for user-profile roles and minor improvement * supported unit test for inheritet roles * added unit test for user-profile-image and minor improvement * minor fixes * cypress fixes * fix the user icon and name in case of no display name * changes as per comments
This commit is contained in:
parent
d1898ffbbc
commit
962c3d6591
@ -127,6 +127,14 @@ describe('DataConsumer Edit policy should work properly', () => {
|
|||||||
CREDENTIALS.lastName
|
CREDENTIALS.lastName
|
||||||
).then((id) => {
|
).then((id) => {
|
||||||
CREDENTIALS.id = id;
|
CREDENTIALS.id = id;
|
||||||
|
|
||||||
|
cy.clickOutside();
|
||||||
|
|
||||||
|
// click the collapse button to open the other details
|
||||||
|
cy.get(
|
||||||
|
'[data-testid="user-profile"] .ant-collapse-expand-icon > .anticon > svg'
|
||||||
|
).click();
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-testid="user-profile"] [data-testid="user-profile-inherited-roles"]'
|
'[data-testid="user-profile"] [data-testid="user-profile-inherited-roles"]'
|
||||||
).should('contain', policy);
|
).should('contain', policy);
|
||||||
|
|||||||
@ -107,6 +107,11 @@ describe('Test Add role and assign it to the user', () => {
|
|||||||
cy.get(`[data-testid="${userName}"]`).click();
|
cy.get(`[data-testid="${userName}"]`).click();
|
||||||
verifyResponseStatusCode('@userDetailsPage', 200);
|
verifyResponseStatusCode('@userDetailsPage', 200);
|
||||||
|
|
||||||
|
// click the collapse button to open the other details
|
||||||
|
cy.get(
|
||||||
|
'[data-testid="user-profile"] .ant-collapse-expand-icon > .anticon > svg'
|
||||||
|
).click();
|
||||||
|
|
||||||
cy.get(
|
cy.get(
|
||||||
'[data-testid="user-profile"] [data-testid="user-profile-roles"]'
|
'[data-testid="user-profile"] [data-testid="user-profile-roles"]'
|
||||||
).should('contain', roleName);
|
).should('contain', roleName);
|
||||||
|
|||||||
@ -1034,10 +1034,10 @@ const TeamDetailsV1 = ({
|
|||||||
<Collapse
|
<Collapse
|
||||||
accordion
|
accordion
|
||||||
bordered={false}
|
bordered={false}
|
||||||
className="site-collapse-custom-collapse"
|
className="header-collapse-custom-collapse"
|
||||||
expandIconPosition="end">
|
expandIconPosition="end">
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
className="site-collapse-custom-panel"
|
className="header-collapse-custom-panel"
|
||||||
collapsible="icon"
|
collapsible="icon"
|
||||||
header={teamsCollapseHeader}
|
header={teamsCollapseHeader}
|
||||||
key="1">
|
key="1">
|
||||||
|
|||||||
@ -113,17 +113,6 @@
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-collapse-custom-collapse .site-collapse-custom-panel {
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0;
|
|
||||||
background: @user-profile-background;
|
|
||||||
border: 0px;
|
|
||||||
|
|
||||||
.ant-collapse-content-box {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.team-assets-right-panel {
|
.team-assets-right-panel {
|
||||||
.summary-panel-container {
|
.summary-panel-container {
|
||||||
|
|||||||
@ -110,7 +110,7 @@ export const UserProfileIcon = () => {
|
|||||||
}, [profilePicture]);
|
}, [profilePicture]);
|
||||||
|
|
||||||
const { userName, teams, roles, inheritedRoles, personas } = useMemo(() => {
|
const { userName, teams, roles, inheritedRoles, personas } = useMemo(() => {
|
||||||
const userName = currentUser?.displayName ?? currentUser?.name ?? TERM_USER;
|
const userName = getEntityName(currentUser) || TERM_USER;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userName,
|
userName,
|
||||||
|
|||||||
@ -12,11 +12,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
findByRole,
|
||||||
findByTestId,
|
findByTestId,
|
||||||
findByText,
|
findByText,
|
||||||
queryByTestId,
|
queryByTestId,
|
||||||
render,
|
render,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { mockTeamsData, mockUserData, mockUserRole } from './mocks/User.mocks';
|
import { mockTeamsData, mockUserData, mockUserRole } from './mocks/User.mocks';
|
||||||
@ -174,7 +176,22 @@ describe('Test User Component', () => {
|
|||||||
container,
|
container,
|
||||||
'UserProfileDetails'
|
'UserProfileDetails'
|
||||||
);
|
);
|
||||||
const UserProfileImage = await findByText(container, 'UserProfileImage');
|
|
||||||
|
expect(UserProfileDetails).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('User profile should render when open collapsible header', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Users userData={mockUserData} {...mockProp} />,
|
||||||
|
{
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const collapsibleButton = await findByRole(container, 'img');
|
||||||
|
|
||||||
|
userEvent.click(collapsibleButton);
|
||||||
|
|
||||||
const UserProfileInheritedRoles = await findByText(
|
const UserProfileInheritedRoles = await findByText(
|
||||||
container,
|
container,
|
||||||
'UserProfileInheritedRoles'
|
'UserProfileInheritedRoles'
|
||||||
@ -183,8 +200,6 @@ describe('Test User Component', () => {
|
|||||||
|
|
||||||
const UserProfileTeams = await findByText(container, 'UserProfileTeams');
|
const UserProfileTeams = await findByText(container, 'UserProfileTeams');
|
||||||
|
|
||||||
expect(UserProfileDetails).toBeInTheDocument();
|
|
||||||
expect(UserProfileImage).toBeInTheDocument();
|
|
||||||
expect(UserProfileRoles).toBeInTheDocument();
|
expect(UserProfileRoles).toBeInTheDocument();
|
||||||
expect(UserProfileTeams).toBeInTheDocument();
|
expect(UserProfileTeams).toBeInTheDocument();
|
||||||
expect(UserProfileInheritedRoles).toBeInTheDocument();
|
expect(UserProfileInheritedRoles).toBeInTheDocument();
|
||||||
@ -212,7 +227,7 @@ describe('Test User Component', () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const datasetContainer = await findByTestId(container, 'table-container');
|
const datasetContainer = await findByTestId(container, 'user-profile');
|
||||||
|
|
||||||
expect(datasetContainer).toBeInTheDocument();
|
expect(datasetContainer).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,9 +11,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Col, Row, Tabs, Typography } from 'antd';
|
import { Col, Collapse, Row, Space, Tabs, Typography } from 'antd';
|
||||||
import Card from 'antd/lib/card/Card';
|
import Card from 'antd/lib/card/Card';
|
||||||
import { noop } from 'lodash';
|
import { isEmpty, noop } from 'lodash';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -22,19 +22,17 @@ import { ReactComponent as PersonaIcon } from '../../assets/svg/ic-personas.svg'
|
|||||||
import ActivityFeedProvider from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
|
import ActivityFeedProvider from '../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
|
||||||
import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
|
import { ActivityFeedTab } from '../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
|
||||||
import TabsLabel from '../../components/TabsLabel/TabsLabel.component';
|
import TabsLabel from '../../components/TabsLabel/TabsLabel.component';
|
||||||
import {
|
import { getUserPath, ROUTES } from '../../constants/constants';
|
||||||
getUserPath,
|
|
||||||
NO_DATA_PLACEHOLDER,
|
|
||||||
ROUTES,
|
|
||||||
} from '../../constants/constants';
|
|
||||||
import { myDataSearchIndex } from '../../constants/Mydata.constants';
|
import { myDataSearchIndex } from '../../constants/Mydata.constants';
|
||||||
import { EntityType } from '../../enums/entity.enum';
|
import { EntityType } from '../../enums/entity.enum';
|
||||||
import { EntityReference } from '../../generated/entity/type';
|
import { EntityReference } from '../../generated/entity/type';
|
||||||
import { useAuth } from '../../hooks/authHooks';
|
import { useAuth } from '../../hooks/authHooks';
|
||||||
import { searchData } from '../../rest/miscAPI';
|
import { searchData } from '../../rest/miscAPI';
|
||||||
|
import { getEntityName } from '../../utils/EntityUtils';
|
||||||
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
|
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
|
||||||
import { useAuthContext } from '../authentication/auth-provider/AuthProvider';
|
import { useAuthContext } from '../authentication/auth-provider/AuthProvider';
|
||||||
import Chip from '../common/Chip/Chip.component';
|
import Chip from '../common/Chip/Chip.component';
|
||||||
|
import DescriptionV1 from '../common/description/DescriptionV1';
|
||||||
import PageLayoutV1 from '../containers/PageLayoutV1';
|
import PageLayoutV1 from '../containers/PageLayoutV1';
|
||||||
import EntitySummaryPanel from '../Explore/EntitySummaryPanel/EntitySummaryPanel.component';
|
import EntitySummaryPanel from '../Explore/EntitySummaryPanel/EntitySummaryPanel.component';
|
||||||
import { EntityDetailsObjectInterface } from '../Explore/explore.interface';
|
import { EntityDetailsObjectInterface } from '../Explore/explore.interface';
|
||||||
@ -47,7 +45,6 @@ import { PersonaSelectableList } from '../Persona/PersonaSelectableList/PersonaS
|
|||||||
import { Props, UserPageTabs } from './Users.interface';
|
import { Props, UserPageTabs } from './Users.interface';
|
||||||
import './Users.style.less';
|
import './Users.style.less';
|
||||||
import UserProfileDetails from './UsersProfile/UserProfileDetails/UserProfileDetails.component';
|
import UserProfileDetails from './UsersProfile/UserProfileDetails/UserProfileDetails.component';
|
||||||
import UserProfileImage from './UsersProfile/UserProfileImage/UserProfileImage.component';
|
|
||||||
import UserProfileInheritedRoles from './UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component';
|
import UserProfileInheritedRoles from './UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component';
|
||||||
import UserProfileRoles from './UsersProfile/UserProfileRoles/UserProfileRoles.component';
|
import UserProfileRoles from './UsersProfile/UserProfileRoles/UserProfileRoles.component';
|
||||||
import UserProfileTeams from './UsersProfile/UserProfileTeams/UserProfileTeams.component';
|
import UserProfileTeams from './UsersProfile/UserProfileTeams/UserProfileTeams.component';
|
||||||
@ -66,18 +63,22 @@ const Users = ({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { currentUser } = useAuthContext();
|
const { currentUser } = useAuthContext();
|
||||||
|
|
||||||
const isSelfProfileView = userData?.id === currentUser?.id;
|
|
||||||
|
|
||||||
const [previewAsset, setPreviewAsset] =
|
const [previewAsset, setPreviewAsset] =
|
||||||
useState<EntityDetailsObjectInterface>();
|
useState<EntityDetailsObjectInterface>();
|
||||||
|
|
||||||
|
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const defaultPersona = useMemo(() => {
|
const isLoggedInUser = useMemo(
|
||||||
return userData.personas?.find(
|
() => username === currentUser?.name,
|
||||||
(persona) => persona.id === userData.defaultPersona?.id
|
[username]
|
||||||
);
|
);
|
||||||
}, [userData]);
|
|
||||||
|
const hasEditPermission = useMemo(
|
||||||
|
() => isAdminUser || isLoggedInUser,
|
||||||
|
[isAdminUser, isLoggedInUser]
|
||||||
|
);
|
||||||
|
|
||||||
const fetchAssetsCount = async (query: string) => {
|
const fetchAssetsCount = async (query: string) => {
|
||||||
try {
|
try {
|
||||||
@ -111,13 +112,6 @@ const Users = ({
|
|||||||
[updateUserDetails, userData]
|
[updateUserDetails, userData]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDefaultPersonaUpdate = useCallback(
|
|
||||||
async (defaultPersona?: EntityReference) => {
|
|
||||||
await updateUserDetails({ ...userData, defaultPersona });
|
|
||||||
},
|
|
||||||
[updateUserDetails, userData]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabDataRender = useCallback(
|
const tabDataRender = useCallback(
|
||||||
(props: {
|
(props: {
|
||||||
queryFilter: string;
|
queryFilter: string;
|
||||||
@ -212,6 +206,63 @@ const Users = ({
|
|||||||
[activeTab, userData, username, setPreviewAsset, tabDataRender]
|
[activeTab, userData, username, setPreviewAsset, tabDataRender]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDescriptionChange = useCallback(
|
||||||
|
async (description: string) => {
|
||||||
|
await updateUserDetails({ description });
|
||||||
|
|
||||||
|
setIsDescriptionEdit(false);
|
||||||
|
},
|
||||||
|
[updateUserDetails, setIsDescriptionEdit]
|
||||||
|
);
|
||||||
|
|
||||||
|
const descriptionRenderComponent = useMemo(
|
||||||
|
() =>
|
||||||
|
hasEditPermission ? (
|
||||||
|
<DescriptionV1
|
||||||
|
description={userData.description ?? ''}
|
||||||
|
entityName={getEntityName(userData as unknown as EntityReference)}
|
||||||
|
entityType={EntityType.USER}
|
||||||
|
hasEditAccess={hasEditPermission}
|
||||||
|
isEdit={isDescriptionEdit}
|
||||||
|
showCommentsIcon={false}
|
||||||
|
onCancel={() => setIsDescriptionEdit(false)}
|
||||||
|
onDescriptionEdit={() => setIsDescriptionEdit(true)}
|
||||||
|
onDescriptionUpdate={handleDescriptionChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Space direction="vertical" size="middle">
|
||||||
|
<Typography.Text className="right-panel-label">
|
||||||
|
{t('label.description')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Paragraph className="m-b-0">
|
||||||
|
{isEmpty(userData.description)
|
||||||
|
? t('label.no-entity', {
|
||||||
|
entity: t('label.description'),
|
||||||
|
})
|
||||||
|
: userData.description}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
[
|
||||||
|
userData,
|
||||||
|
isAdminUser,
|
||||||
|
isDescriptionEdit,
|
||||||
|
hasEditPermission,
|
||||||
|
getEntityName,
|
||||||
|
handleDescriptionChange,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const userProfileCollapseHeader = useMemo(
|
||||||
|
() => (
|
||||||
|
<UserProfileDetails
|
||||||
|
updateUserDetails={updateUserDetails}
|
||||||
|
userData={userData}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[userData, updateUserDetails]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ([UserPageTabs.MY_DATA, UserPageTabs.FOLLOWING].includes(activeTab)) {
|
if ([UserPageTabs.MY_DATA, UserPageTabs.FOLLOWING].includes(activeTab)) {
|
||||||
fetchAssetsCount(
|
fetchAssetsCount(
|
||||||
@ -224,97 +275,72 @@ const Users = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayoutV1 className="user-layout h-full" pageTitle={t('label.user')}>
|
<PageLayoutV1 className="user-layout h-full" pageTitle={t('label.user')}>
|
||||||
<div data-testid="table-container">
|
<div data-testid="user-profile">
|
||||||
<Row className="user-profile-container" data-testid="user-profile">
|
<Collapse
|
||||||
<Col className="flex-center border-right" span={4}>
|
accordion
|
||||||
<UserProfileImage
|
bordered={false}
|
||||||
userData={{
|
className="header-collapse-custom-collapse user-profile-container"
|
||||||
id: userData.id,
|
expandIconPosition="end">
|
||||||
name: userData.name,
|
<Collapse.Panel
|
||||||
displayName: userData.displayName,
|
className="header-collapse-custom-panel"
|
||||||
images: userData.profile?.images,
|
collapsible="icon"
|
||||||
}}
|
header={userProfileCollapseHeader}
|
||||||
/>
|
key="1">
|
||||||
</Col>
|
<Row className="border-top p-y-lg" gutter={[0, 24]}>
|
||||||
<Col className="p-x-sm border-right" span={5}>
|
<Col span={24}>
|
||||||
<UserProfileDetails
|
<Row data-testid="user-profile-details">
|
||||||
updateUserDetails={updateUserDetails}
|
<Col className="p-x-sm border-right" span={6}>
|
||||||
userData={{
|
<UserProfileTeams
|
||||||
email: userData.email,
|
teams={userData.teams}
|
||||||
name: userData.name,
|
updateUserDetails={updateUserDetails}
|
||||||
displayName: userData.displayName,
|
|
||||||
description: userData.description,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col className="p-x-sm border-right" span={5}>
|
|
||||||
<UserProfileTeams
|
|
||||||
teams={userData.teams}
|
|
||||||
updateUserDetails={updateUserDetails}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col className="p-x-sm border-right" span={5}>
|
|
||||||
<div className="d-flex flex-col justify-between h-full">
|
|
||||||
<UserProfileRoles
|
|
||||||
isUserAdmin={userData.isAdmin}
|
|
||||||
updateUserDetails={updateUserDetails}
|
|
||||||
userRoles={userData.roles}
|
|
||||||
/>
|
|
||||||
<UserProfileInheritedRoles
|
|
||||||
inheritedRoles={userData.inheritedRoles}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col className="p-x-sm border-right" span={5}>
|
|
||||||
<div className="d-flex flex-col justify-between h-full">
|
|
||||||
<Card
|
|
||||||
className="ant-card-feed relative card-body-border-none card-padding-y-0 m-b-md"
|
|
||||||
title={
|
|
||||||
<Typography.Text
|
|
||||||
className="right-panel-label items-center d-flex gap-2"
|
|
||||||
data-testid="inherited-roles">
|
|
||||||
{t('label.persona')}
|
|
||||||
<PersonaSelectableList
|
|
||||||
multiSelect
|
|
||||||
hasPermission={Boolean(isAdminUser)}
|
|
||||||
selectedPersonas={userData.personas ?? []}
|
|
||||||
onUpdate={handlePersonaUpdate}
|
|
||||||
/>
|
/>
|
||||||
</Typography.Text>
|
</Col>
|
||||||
}>
|
<Col className="p-x-sm border-right" span={6}>
|
||||||
<Chip
|
<UserProfileRoles
|
||||||
showNoDataPlaceholder
|
updateUserDetails={updateUserDetails}
|
||||||
data={userData.personas ?? []}
|
userRoles={userData.roles}
|
||||||
icon={<PersonaIcon height={18} />}
|
|
||||||
noDataPlaceholder={t('message.no-persona-assigned')}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
className="ant-card-feed relative card-body-border-none card-padding-y-0"
|
|
||||||
title={
|
|
||||||
<Typography.Text
|
|
||||||
className="right-panel-label m-b-0 d-flex gap-2"
|
|
||||||
data-testid="inherited-roles">
|
|
||||||
{t('label.default-persona')}
|
|
||||||
<PersonaSelectableList
|
|
||||||
hasPermission={isAdminUser || isSelfProfileView}
|
|
||||||
multiSelect={false}
|
|
||||||
personaList={userData.personas}
|
|
||||||
selectedPersonas={defaultPersona ? [defaultPersona] : []}
|
|
||||||
onUpdate={handleDefaultPersonaUpdate}
|
|
||||||
/>
|
/>
|
||||||
</Typography.Text>
|
</Col>
|
||||||
}>
|
<Col className="p-x-sm border-right" span={6}>
|
||||||
<Chip
|
<UserProfileInheritedRoles
|
||||||
showNoDataPlaceholder
|
inheritedRoles={userData.inheritedRoles}
|
||||||
data={defaultPersona ? [defaultPersona] : []}
|
/>
|
||||||
icon={<PersonaIcon height={18} />}
|
</Col>
|
||||||
noDataPlaceholder={NO_DATA_PLACEHOLDER}
|
<Col className="p-x-sm" span={6}>
|
||||||
/>
|
<div className="d-flex flex-col justify-between h-full">
|
||||||
</Card>
|
<Card
|
||||||
</div>
|
className="ant-card-feed relative card-body-border-none card-padding-y-0"
|
||||||
</Col>
|
title={
|
||||||
</Row>
|
<Typography.Text
|
||||||
|
className="right-panel-label items-center d-flex gap-2"
|
||||||
|
data-testid="inherited-roles">
|
||||||
|
{t('label.persona')}
|
||||||
|
<PersonaSelectableList
|
||||||
|
multiSelect
|
||||||
|
hasPermission={Boolean(isAdminUser)}
|
||||||
|
selectedPersonas={userData.personas ?? []}
|
||||||
|
onUpdate={handlePersonaUpdate}
|
||||||
|
/>
|
||||||
|
</Typography.Text>
|
||||||
|
}>
|
||||||
|
<Chip
|
||||||
|
showNoDataPlaceholder
|
||||||
|
data={userData.personas ?? []}
|
||||||
|
icon={<PersonaIcon height={14} />}
|
||||||
|
noDataPlaceholder={t('message.no-persona-assigned')}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<Col className="border-top p-lg p-b-0" span={24}>
|
||||||
|
{descriptionRenderComponent}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Collapse.Panel>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
destroyInactiveTabPane
|
destroyInactiveTabPane
|
||||||
activeKey={activeTab ?? UserPageTabs.ACTIVITY}
|
activeKey={activeTab ?? UserPageTabs.ACTIVITY}
|
||||||
|
|||||||
@ -30,17 +30,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-layout {
|
.user-layout {
|
||||||
.ant-col {
|
> .ant-col {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-profile-container {
|
.user-profile-container {
|
||||||
padding: 30px 0;
|
|
||||||
background: @user-profile-background;
|
|
||||||
|
|
||||||
.profile-image-container {
|
.profile-image-container {
|
||||||
width: 190px;
|
width: 60px;
|
||||||
height: 190px;
|
height: 60px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 3px solid @white;
|
border: 3px solid @white;
|
||||||
|
|||||||
@ -11,21 +11,20 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Button, Col, Input, Row, Space, Typography } from 'antd';
|
import { Button, Divider, Input, Space, Typography } from 'antd';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg';
|
import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg';
|
||||||
import { useAuthContext } from '../../../../components/authentication/auth-provider/AuthProvider';
|
import { useAuthContext } from '../../../../components/authentication/auth-provider/AuthProvider';
|
||||||
import DescriptionV1 from '../../../../components/common/description/DescriptionV1';
|
|
||||||
import InlineEdit from '../../../../components/InlineEdit/InlineEdit.component';
|
import InlineEdit from '../../../../components/InlineEdit/InlineEdit.component';
|
||||||
import ChangePasswordForm from '../../../../components/Users/ChangePasswordForm';
|
import ChangePasswordForm from '../../../../components/Users/ChangePasswordForm';
|
||||||
import {
|
import {
|
||||||
DE_ACTIVE_COLOR,
|
DE_ACTIVE_COLOR,
|
||||||
ICON_DIMENSION,
|
ICON_DIMENSION,
|
||||||
|
NO_DATA_PLACEHOLDER,
|
||||||
} from '../../../../constants/constants';
|
} from '../../../../constants/constants';
|
||||||
import { EntityType } from '../../../../enums/entity.enum';
|
|
||||||
import {
|
import {
|
||||||
ChangePasswordRequest,
|
ChangePasswordRequest,
|
||||||
RequestType,
|
RequestType,
|
||||||
@ -36,6 +35,9 @@ import { useAuth } from '../../../../hooks/authHooks';
|
|||||||
import { changePassword } from '../../../../rest/auth-API';
|
import { changePassword } from '../../../../rest/auth-API';
|
||||||
import { getEntityName } from '../../../../utils/EntityUtils';
|
import { getEntityName } from '../../../../utils/EntityUtils';
|
||||||
import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils';
|
import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils';
|
||||||
|
import Chip from '../../../common/Chip/Chip.component';
|
||||||
|
import { PersonaSelectableList } from '../../../Persona/PersonaSelectableList/PersonaSelectableList.component';
|
||||||
|
import UserProfileImage from '../UserProfileImage/UserProfileImage.component';
|
||||||
import { UserProfileDetailsProps } from './UserProfileDetails.interface';
|
import { UserProfileDetailsProps } from './UserProfileDetails.interface';
|
||||||
|
|
||||||
const UserProfileDetails = ({
|
const UserProfileDetails = ({
|
||||||
@ -52,7 +54,11 @@ const UserProfileDetails = ({
|
|||||||
const [isChangePassword, setIsChangePassword] = useState<boolean>(false);
|
const [isChangePassword, setIsChangePassword] = useState<boolean>(false);
|
||||||
const [displayName, setDisplayName] = useState(userData.displayName);
|
const [displayName, setDisplayName] = useState(userData.displayName);
|
||||||
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
|
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
|
||||||
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
|
|
||||||
|
const isSelfProfileView = useMemo(
|
||||||
|
() => userData?.id === currentUser?.id,
|
||||||
|
[userData, currentUser]
|
||||||
|
);
|
||||||
|
|
||||||
const isAuthProviderBasic = useMemo(
|
const isAuthProviderBasic = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -71,28 +77,38 @@ const UserProfileDetails = ({
|
|||||||
[isAdminUser, isLoggedInUser]
|
[isAdminUser, isLoggedInUser]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const hasPersonaEditPermission = useMemo(
|
||||||
|
() => isAdminUser || isSelfProfileView,
|
||||||
|
[isAdminUser, isSelfProfileView]
|
||||||
|
);
|
||||||
|
|
||||||
|
const showChangePasswordComponent = useMemo(
|
||||||
|
() => isAuthProviderBasic && hasEditPermission,
|
||||||
|
[isAuthProviderBasic, hasEditPermission]
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultPersona = useMemo(
|
||||||
|
() =>
|
||||||
|
userData.personas?.find(
|
||||||
|
(persona) => persona.id === userData.defaultPersona?.id
|
||||||
|
),
|
||||||
|
[userData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
setDisplayName(e.target.value);
|
setDisplayName(e.target.value);
|
||||||
};
|
|
||||||
|
|
||||||
const handleDescriptionChange = async (description: string) => {
|
const handleDisplayNameSave = useCallback(() => {
|
||||||
await updateUserDetails({ description });
|
|
||||||
|
|
||||||
setIsDescriptionEdit(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisplayNameSave = () => {
|
|
||||||
if (displayName !== userData.displayName) {
|
if (displayName !== userData.displayName) {
|
||||||
updateUserDetails({ displayName: displayName ?? '' });
|
updateUserDetails({ displayName: displayName ?? '' });
|
||||||
}
|
}
|
||||||
setIsDisplayNameEdit(false);
|
setIsDisplayNameEdit(false);
|
||||||
};
|
}, [userData.displayName, displayName, updateUserDetails]);
|
||||||
|
|
||||||
const displayNameRenderComponent = useMemo(
|
const displayNameRenderComponent = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isDisplayNameEdit && hasEditPermission ? (
|
isDisplayNameEdit && hasEditPermission ? (
|
||||||
<InlineEdit
|
<InlineEdit
|
||||||
direction="vertical"
|
|
||||||
onCancel={() => setIsDisplayNameEdit(false)}
|
onCancel={() => setIsDisplayNameEdit(false)}
|
||||||
onSave={handleDisplayNameSave}>
|
onSave={handleDisplayNameSave}>
|
||||||
<Input
|
<Input
|
||||||
@ -107,32 +123,31 @@ const UserProfileDetails = ({
|
|||||||
/>
|
/>
|
||||||
</InlineEdit>
|
</InlineEdit>
|
||||||
) : (
|
) : (
|
||||||
<Row align="middle" wrap={false}>
|
<Space align="center">
|
||||||
<Col flex="auto">
|
<Typography.Text
|
||||||
<Typography.Text
|
className="font-medium text-md"
|
||||||
className="text-lg font-medium"
|
data-testid="user-name"
|
||||||
ellipsis={{ tooltip: true }}>
|
ellipsis={{ tooltip: true }}
|
||||||
{hasEditPermission
|
style={{ maxWidth: '400px' }}>
|
||||||
? userData.displayName ||
|
{hasEditPermission
|
||||||
t('label.add-entity', { entity: t('label.display-name') })
|
? userData.displayName ||
|
||||||
: getEntityName(userData)}
|
t('label.add-entity', { entity: t('label.display-name') })
|
||||||
</Typography.Text>
|
: getEntityName(userData)}
|
||||||
</Col>
|
</Typography.Text>
|
||||||
<Col className="d-flex justify-end" flex="25px">
|
{hasEditPermission && (
|
||||||
{hasEditPermission && (
|
<EditIcon
|
||||||
<EditIcon
|
className="cursor-pointer align-middle"
|
||||||
className="cursor-pointer"
|
color={DE_ACTIVE_COLOR}
|
||||||
color={DE_ACTIVE_COLOR}
|
data-testid="edit-displayName"
|
||||||
data-testid="edit-displayName"
|
{...ICON_DIMENSION}
|
||||||
{...ICON_DIMENSION}
|
onClick={() => setIsDisplayNameEdit(true)}
|
||||||
onClick={() => setIsDisplayNameEdit(true)}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</Space>
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
),
|
),
|
||||||
[
|
[
|
||||||
userData,
|
userData,
|
||||||
|
displayName,
|
||||||
isDisplayNameEdit,
|
isDisplayNameEdit,
|
||||||
hasEditPermission,
|
hasEditPermission,
|
||||||
getEntityName,
|
getEntityName,
|
||||||
@ -141,48 +156,12 @@ const UserProfileDetails = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const descriptionRenderComponent = useMemo(
|
|
||||||
() =>
|
|
||||||
hasEditPermission ? (
|
|
||||||
<DescriptionV1
|
|
||||||
reduceDescription
|
|
||||||
description={userData.description ?? ''}
|
|
||||||
entityName={getEntityName(userData as unknown as EntityReference)}
|
|
||||||
entityType={EntityType.USER}
|
|
||||||
hasEditAccess={isAdminUser}
|
|
||||||
isEdit={isDescriptionEdit}
|
|
||||||
showCommentsIcon={false}
|
|
||||||
onCancel={() => setIsDescriptionEdit(false)}
|
|
||||||
onDescriptionEdit={() => setIsDescriptionEdit(true)}
|
|
||||||
onDescriptionUpdate={handleDescriptionChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Typography.Paragraph className="m-b-0">
|
|
||||||
{userData.description ?? (
|
|
||||||
<span className="text-grey-muted">
|
|
||||||
{t('label.no-entity', {
|
|
||||||
entity: t('label.description'),
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Typography.Paragraph>
|
|
||||||
),
|
|
||||||
[
|
|
||||||
userData,
|
|
||||||
isAdminUser,
|
|
||||||
isDescriptionEdit,
|
|
||||||
hasEditPermission,
|
|
||||||
getEntityName,
|
|
||||||
handleDescriptionChange,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const changePasswordRenderComponent = useMemo(
|
const changePasswordRenderComponent = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isAuthProviderBasic &&
|
showChangePasswordComponent && (
|
||||||
(isAdminUser || isLoggedInUser) && (
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full text-xs"
|
className="w-full text-xs"
|
||||||
|
data-testid="change-password-button"
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => setIsChangePassword(true)}>
|
onClick={() => setIsChangePassword(true)}>
|
||||||
{t('label.change-entity', {
|
{t('label.change-entity', {
|
||||||
@ -190,7 +169,7 @@ const UserProfileDetails = ({
|
|||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
[isAuthProviderBasic, isAdminUser, isLoggedInUser]
|
[showChangePasswordComponent]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChangePassword = async (data: ChangePasswordRequest) => {
|
const handleChangePassword = async (data: ChangePasswordRequest) => {
|
||||||
@ -216,34 +195,93 @@ const UserProfileDetails = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const userEmailRender = useMemo(
|
||||||
<Space
|
() => (
|
||||||
className="p-sm w-full"
|
<Space align="center">
|
||||||
data-testid="user-profile-details"
|
<Typography.Text
|
||||||
direction="vertical"
|
className="text-grey-muted"
|
||||||
size="middle">
|
data-testid="user-email-label">{`${t(
|
||||||
<Space className="w-full" direction="vertical" size={2}>
|
'label.email'
|
||||||
{displayNameRenderComponent}
|
)} :`}</Typography.Text>
|
||||||
<Typography.Paragraph
|
|
||||||
className="m-b-0 text-grey-muted"
|
<Typography.Paragraph className="m-b-0" data-testid="user-email-value">
|
||||||
ellipsis={{ tooltip: true }}>
|
|
||||||
{userData.email}
|
{userData.email}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</Space>
|
</Space>
|
||||||
|
),
|
||||||
|
[userData.email]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDefaultPersonaUpdate = useCallback(
|
||||||
|
async (defaultPersona?: EntityReference) => {
|
||||||
|
await updateUserDetails({ ...userData, defaultPersona });
|
||||||
|
},
|
||||||
|
[updateUserDetails, userData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultPersonaRender = useMemo(
|
||||||
|
() => (
|
||||||
|
<Space align="center">
|
||||||
|
<Typography.Text
|
||||||
|
className="text-grey-muted"
|
||||||
|
data-testid="default-persona-label">
|
||||||
|
{`${t('label.default-persona')} :`}
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<Chip
|
||||||
|
showNoDataPlaceholder
|
||||||
|
data={defaultPersona ? [defaultPersona] : []}
|
||||||
|
noDataPlaceholder={NO_DATA_PLACEHOLDER}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PersonaSelectableList
|
||||||
|
hasPermission={hasPersonaEditPermission}
|
||||||
|
multiSelect={false}
|
||||||
|
personaList={userData.personas}
|
||||||
|
selectedPersonas={defaultPersona ? [defaultPersona] : []}
|
||||||
|
onUpdate={handleDefaultPersonaUpdate}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
[
|
||||||
|
userData.personas,
|
||||||
|
hasPersonaEditPermission,
|
||||||
|
defaultPersona,
|
||||||
|
handleDefaultPersonaUpdate,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Space
|
||||||
|
wrap
|
||||||
|
className="w-full justify-between"
|
||||||
|
data-testid="user-profile-details"
|
||||||
|
size="middle">
|
||||||
|
<Space className="w-full">
|
||||||
|
<UserProfileImage userData={userData} />
|
||||||
|
{displayNameRenderComponent}
|
||||||
|
<Divider type="vertical" />
|
||||||
|
|
||||||
|
{userEmailRender}
|
||||||
|
<Divider type="vertical" />
|
||||||
|
|
||||||
|
{defaultPersonaRender}
|
||||||
|
</Space>
|
||||||
|
|
||||||
<Space direction="vertical" size="middle">
|
|
||||||
{descriptionRenderComponent}
|
|
||||||
{changePasswordRenderComponent}
|
{changePasswordRenderComponent}
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<ChangePasswordForm
|
{showChangePasswordComponent && (
|
||||||
isLoading={isLoading}
|
<ChangePasswordForm
|
||||||
isLoggedInUser={isLoggedInUser}
|
isLoading={isLoading}
|
||||||
visible={isChangePassword}
|
isLoggedInUser={isLoggedInUser}
|
||||||
onCancel={() => setIsChangePassword(false)}
|
visible={isChangePassword}
|
||||||
onSave={(data) => handleChangePassword(data)}
|
onCancel={() => setIsChangePassword(false)}
|
||||||
/>
|
onSave={(data) => handleChangePassword(data)}
|
||||||
</Space>
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -14,11 +14,6 @@
|
|||||||
import { User } from '../../../../generated/entity/teams/user';
|
import { User } from '../../../../generated/entity/teams/user';
|
||||||
|
|
||||||
export interface UserProfileDetailsProps {
|
export interface UserProfileDetailsProps {
|
||||||
userData: {
|
userData: User;
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
displayName?: string;
|
|
||||||
description?: string;
|
|
||||||
};
|
|
||||||
updateUserDetails: (data: Partial<User>) => Promise<void>;
|
updateUserDetails: (data: Partial<User>) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* 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 { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { AuthProvider } from '../../../../generated/settings/settings';
|
||||||
|
import { useAuth } from '../../../../hooks/authHooks';
|
||||||
|
import { USER_DATA } from '../../../../mocks/User.mock';
|
||||||
|
import { useAuthContext } from '../../../authentication/auth-provider/AuthProvider';
|
||||||
|
import UserProfileDetails from './UserProfileDetails.component';
|
||||||
|
import { UserProfileDetailsProps } from './UserProfileDetails.interface';
|
||||||
|
|
||||||
|
const mockParams = {
|
||||||
|
fqn: 'test',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPropsData: UserProfileDetailsProps = {
|
||||||
|
userData: USER_DATA,
|
||||||
|
updateUserDetails: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useParams: jest.fn().mockImplementation(() => mockParams),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'../../../../components/authentication/auth-provider/AuthProvider',
|
||||||
|
() => ({
|
||||||
|
useAuthContext: jest.fn(() => ({
|
||||||
|
authConfig: {
|
||||||
|
provider: AuthProvider.Basic,
|
||||||
|
},
|
||||||
|
currentUser: {
|
||||||
|
name: 'test',
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('../../../../hooks/authHooks', () => ({
|
||||||
|
useAuth: jest.fn().mockReturnValue({ isAdminUser: true }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../../utils/EntityUtils', () => ({
|
||||||
|
getEntityName: jest.fn().mockReturnValue('entityName'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../../utils/ToastUtils', () => ({
|
||||||
|
showErrorToast: jest.fn(),
|
||||||
|
showSuccessToast: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../UserProfileImage/UserProfileImage.component', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>ProfilePicture</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../../../components/InlineEdit/InlineEdit.component', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>InlineEdit</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../../../components/Users/ChangePasswordForm', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>ChangePasswordForm</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'../../../Persona/PersonaSelectableList/PersonaSelectableList.component',
|
||||||
|
() => ({
|
||||||
|
PersonaSelectableList: jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue(<p>PersonaSelectableList</p>),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('../../../common/Chip/Chip.component', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>Chip</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../UserProfileImage/UserProfileImage.component', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>UserProfileImage</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../../../rest/auth-API', () => ({
|
||||||
|
changePassword: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Test User Profile Details Component', () => {
|
||||||
|
it('Should render user profile details component', async () => {
|
||||||
|
render(<UserProfileDetails {...mockPropsData} />, {
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-details')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('ChangePasswordForm')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should render user data component', async () => {
|
||||||
|
render(<UserProfileDetails {...mockPropsData} />, {
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-details')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// if user doesn't have displayname
|
||||||
|
expect(screen.getByTestId('user-name')).toContainHTML('label.add-entity');
|
||||||
|
expect(screen.getByTestId('edit-displayName')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// user email
|
||||||
|
expect(screen.getByTestId('user-email-label')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('user-email-value')).toContainHTML(
|
||||||
|
USER_DATA.email
|
||||||
|
);
|
||||||
|
|
||||||
|
// user default persona along with edit
|
||||||
|
expect(screen.getByTestId('default-persona-label')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('PersonaSelectableList')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('change-password-button')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render change password button and component in case of SSO', async () => {
|
||||||
|
(useAuthContext as jest.Mock).mockImplementationOnce(() => ({
|
||||||
|
authConfig: jest.fn().mockImplementationOnce(() => ({
|
||||||
|
provider: AuthProvider.Google,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<UserProfileDetails {...mockPropsData} />, {
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('change-password-button')
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByText('ChangePasswordForm')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not provide edit access if non admin user and not current user', async () => {
|
||||||
|
(useAuth as jest.Mock).mockImplementationOnce(() => ({
|
||||||
|
isAdminUser: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
(useAuthContext as jest.Mock).mockImplementationOnce(() => ({
|
||||||
|
currentUser: {
|
||||||
|
name: 'admin',
|
||||||
|
id: '1234',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<UserProfileDetails {...mockPropsData} />, {
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-details')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// render user name with no edit if doesn't have edit access
|
||||||
|
expect(screen.getByTestId('user-name')).toContainHTML('entityName');
|
||||||
|
expect(screen.queryByTestId('edit-displayName')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// render chip in case of no default persona to other user
|
||||||
|
expect(screen.getByTestId('default-persona-label')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Chip')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('PersonaSelectableList')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('change-password-button')
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render edit display name input on click', async () => {
|
||||||
|
render(<UserProfileDetails {...mockPropsData} />, {
|
||||||
|
wrapper: MemoryRouter,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-details')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-name')).toContainHTML('label.add-entity');
|
||||||
|
|
||||||
|
const editButton = screen.getByTestId('edit-displayName');
|
||||||
|
|
||||||
|
expect(editButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
|
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -21,7 +21,7 @@ import {
|
|||||||
import { UserProfileImageProps } from './UserProfileImage.interface';
|
import { UserProfileImageProps } from './UserProfileImage.interface';
|
||||||
|
|
||||||
const UserProfileImage = ({ userData }: UserProfileImageProps) => {
|
const UserProfileImage = ({ userData }: UserProfileImageProps) => {
|
||||||
const [isImgUrlValid, SetIsImgUrlValid] = useState<boolean>(true);
|
const [isImgUrlValid, setIsImgUrlValid] = useState<boolean>(true);
|
||||||
|
|
||||||
const image = useMemo(
|
const image = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -30,31 +30,32 @@ const UserProfileImage = ({ userData }: UserProfileImageProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (image) {
|
setIsImgUrlValid(Boolean(image));
|
||||||
SetIsImgUrlValid(true);
|
|
||||||
}
|
|
||||||
}, [image]);
|
}, [image]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-image-container">
|
<div
|
||||||
|
className="profile-image-container"
|
||||||
|
data-testid="profile-image-container">
|
||||||
{isImgUrlValid ? (
|
{isImgUrlValid ? (
|
||||||
<Image
|
<Image
|
||||||
alt="profile"
|
alt="profile"
|
||||||
|
data-testid="user-profile-image"
|
||||||
preview={false}
|
preview={false}
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
src={image ?? ''}
|
src={image ?? ''}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
SetIsImgUrlValid(false);
|
setIsImgUrlValid(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ProfilePicture
|
<ProfilePicture
|
||||||
displayName={userData?.displayName ?? userData.name}
|
displayName={userData?.displayName ?? userData.name}
|
||||||
height="186"
|
height="54"
|
||||||
id={userData?.id ?? ''}
|
id={userData?.id ?? ''}
|
||||||
name={userData?.name ?? ''}
|
name={userData?.name ?? ''}
|
||||||
textClass="text-5xl"
|
textClass="text-xl"
|
||||||
width=""
|
width="54"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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 React from 'react';
|
||||||
|
import { getImageWithResolutionAndFallback } from '../../../../utils/ProfilerUtils';
|
||||||
|
import { mockUserData } from '../../mocks/User.mocks';
|
||||||
|
import UserProfileImage from './UserProfileImage.component';
|
||||||
|
import { UserProfileImageProps } from './UserProfileImage.interface';
|
||||||
|
|
||||||
|
const mockPropsData: UserProfileImageProps = {
|
||||||
|
userData: mockUserData,
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock('../../../../utils/ProfilerUtils', () => ({
|
||||||
|
getImageWithResolutionAndFallback: jest.fn().mockImplementation(undefined),
|
||||||
|
ImageQuality: jest.fn().mockReturnValue('6x'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../../components/common/ProfilePicture/ProfilePicture', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>ProfilePicture</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test User User Profile Image Component', () => {
|
||||||
|
it('should render user profile image component', async () => {
|
||||||
|
render(<UserProfileImage {...mockPropsData} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('profile-image-container')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render user profile picture component if no image found', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<UserProfileImage {...mockPropsData} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId('profile-image-container')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('ProfilePicture')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render user profile picture component with image', async () => {
|
||||||
|
(getImageWithResolutionAndFallback as jest.Mock).mockImplementationOnce(
|
||||||
|
() => '/image/test/png'
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<UserProfileImage {...mockPropsData} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('profile-image-container')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-image')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -31,7 +31,7 @@ const UserProfileInheritedRoles = ({
|
|||||||
title={
|
title={
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
className="right-panel-label m-b-0"
|
className="right-panel-label m-b-0"
|
||||||
data-testid="inherited-roles">
|
data-testid="inherited-roles-label">
|
||||||
{t('label.inherited-role-plural')}
|
{t('label.inherited-role-plural')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
}>
|
}>
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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 { render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import UserProfileInheritedRoles from './UserProfileInheritedRoles.component';
|
||||||
|
import { UserProfileInheritedRolesProps } from './UserProfileInheritedRoles.interface';
|
||||||
|
|
||||||
|
const mockPropsData: UserProfileInheritedRolesProps = {
|
||||||
|
inheritedRoles: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock('../../../common/Chip/Chip.component', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>Chip</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test User Profile Roles Component', () => {
|
||||||
|
it('should render user profile roles component', async () => {
|
||||||
|
render(<UserProfileInheritedRoles {...mockPropsData} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('user-profile-inherited-roles')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('inherited-roles-label')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(await screen.findAllByText('Chip')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -34,7 +34,6 @@ import { showErrorToast } from '../../../../utils/ToastUtils';
|
|||||||
import { UserProfileRolesProps } from './UserProfileRoles.interface';
|
import { UserProfileRolesProps } from './UserProfileRoles.interface';
|
||||||
|
|
||||||
const UserProfileRoles = ({
|
const UserProfileRoles = ({
|
||||||
isUserAdmin,
|
|
||||||
userRoles,
|
userRoles,
|
||||||
updateUserDetails,
|
updateUserDetails,
|
||||||
}: UserProfileRolesProps) => {
|
}: UserProfileRolesProps) => {
|
||||||
@ -53,7 +52,7 @@ const UserProfileRoles = ({
|
|||||||
value: role.id,
|
value: role.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!isUserAdmin) {
|
if (!isAdminUser) {
|
||||||
options.push({
|
options.push({
|
||||||
label: TERM_ADMIN,
|
label: TERM_ADMIN,
|
||||||
value: toLower(TERM_ADMIN),
|
value: toLower(TERM_ADMIN),
|
||||||
@ -61,7 +60,7 @@ const UserProfileRoles = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [roles]);
|
}, [roles, isAdminUser, getEntityName]);
|
||||||
|
|
||||||
const fetchRoles = async () => {
|
const fetchRoles = async () => {
|
||||||
setIsRolesLoading(true);
|
setIsRolesLoading(true);
|
||||||
@ -112,27 +111,27 @@ const UserProfileRoles = ({
|
|||||||
() => (
|
() => (
|
||||||
<Chip
|
<Chip
|
||||||
data={[
|
data={[
|
||||||
...(isUserAdmin
|
...(isAdminUser
|
||||||
? [{ id: 'admin', type: 'role', name: TERM_ADMIN }]
|
? [{ id: 'admin', type: 'role', name: TERM_ADMIN }]
|
||||||
: []),
|
: []),
|
||||||
...(userRoles ?? []),
|
...(userRoles ?? []),
|
||||||
]}
|
]}
|
||||||
icon={<UserIcons height={20} />}
|
icon={<UserIcons height={20} />}
|
||||||
noDataPlaceholder={t('message.no-roles-assigned')}
|
noDataPlaceholder={t('message.no-roles-assigned')}
|
||||||
showNoDataPlaceholder={!isUserAdmin}
|
showNoDataPlaceholder={!isAdminUser}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[userRoles, isUserAdmin]
|
[userRoles, isAdminUser]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const defaultUserRoles = [
|
const defaultUserRoles = [
|
||||||
...(userRoles?.map((role) => role.id) ?? []),
|
...(userRoles?.map((role) => role.id) ?? []),
|
||||||
...(isUserAdmin ? [toLower(TERM_ADMIN)] : []),
|
...(isAdminUser ? [toLower(TERM_ADMIN)] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
setSelectedRoles(defaultUserRoles);
|
setSelectedRoles(defaultUserRoles);
|
||||||
}, [isUserAdmin, userRoles]);
|
}, [isAdminUser, userRoles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRolesEdit && isEmpty(roles)) {
|
if (isRolesEdit && isEmpty(roles)) {
|
||||||
@ -154,7 +153,7 @@ const UserProfileRoles = ({
|
|||||||
<EditIcon
|
<EditIcon
|
||||||
className="cursor-pointer align-middle"
|
className="cursor-pointer align-middle"
|
||||||
color={DE_ACTIVE_COLOR}
|
color={DE_ACTIVE_COLOR}
|
||||||
data-testid="edit-roles"
|
data-testid="edit-roles-button"
|
||||||
{...ICON_DIMENSION}
|
{...ICON_DIMENSION}
|
||||||
onClick={() => setIsRolesEdit(true)}
|
onClick={() => setIsRolesEdit(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
import { User } from '../../../../generated/entity/teams/user';
|
import { User } from '../../../../generated/entity/teams/user';
|
||||||
|
|
||||||
export interface UserProfileRolesProps {
|
export interface UserProfileRolesProps {
|
||||||
isUserAdmin?: boolean;
|
|
||||||
userRoles: User['roles'];
|
userRoles: User['roles'];
|
||||||
updateUserDetails: (data: Partial<User>) => Promise<void>;
|
updateUserDetails: (data: Partial<User>) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* 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 { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useAuth } from '../../../../hooks/authHooks';
|
||||||
|
import { getRoles } from '../../../../rest/rolesAPIV1';
|
||||||
|
import { mockUserRole } from '../../mocks/User.mocks';
|
||||||
|
import UserProfileRoles from './UserProfileRoles.component';
|
||||||
|
import { UserProfileRolesProps } from './UserProfileRoles.interface';
|
||||||
|
|
||||||
|
const mockPropsData: UserProfileRolesProps = {
|
||||||
|
userRoles: [],
|
||||||
|
updateUserDetails: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock('../../../../hooks/authHooks', () => ({
|
||||||
|
useAuth: jest.fn().mockReturnValue({ isAdminUser: false }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../../components/InlineEdit/InlineEdit.component', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>InlineEdit</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../../common/Chip/Chip.component', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>Chip</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../../../utils/EntityUtils', () => ({
|
||||||
|
getEntityName: jest.fn().mockReturnValue('roleName'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../../utils/ToastUtils', () => ({
|
||||||
|
showErrorToast: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../../rest/rolesAPIV1', () => ({
|
||||||
|
getRoles: jest.fn().mockImplementation(() => Promise.resolve(mockUserRole)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Test User Profile Roles Component', () => {
|
||||||
|
it('should render user profile roles component', async () => {
|
||||||
|
render(<UserProfileRoles {...mockPropsData} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-roles')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render chip component', async () => {
|
||||||
|
render(<UserProfileRoles {...mockPropsData} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-roles')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(await screen.findAllByText('Chip')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render roles edit button if non admin user', async () => {
|
||||||
|
render(<UserProfileRoles {...mockPropsData} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-roles')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('edit-roles-button')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render edit button if admin user', async () => {
|
||||||
|
(useAuth as jest.Mock).mockImplementation(() => ({
|
||||||
|
isAdminUser: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<UserProfileRoles {...mockPropsData} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-roles')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('edit-roles-button')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render select field on edit button action', async () => {
|
||||||
|
(useAuth as jest.Mock).mockImplementation(() => ({
|
||||||
|
isAdminUser: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<UserProfileRoles {...mockPropsData} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-roles')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const editButton = screen.getByTestId('edit-roles-button');
|
||||||
|
|
||||||
|
expect(editButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
|
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call roles api on edit button action', async () => {
|
||||||
|
(useAuth as jest.Mock).mockImplementation(() => ({
|
||||||
|
isAdminUser: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<UserProfileRoles {...mockPropsData} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-profile-roles')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const editButton = screen.getByTestId('edit-roles-button');
|
||||||
|
|
||||||
|
expect(editButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
|
expect(getRoles).toHaveBeenCalledWith('', undefined, undefined, false, 50);
|
||||||
|
|
||||||
|
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -64,6 +64,7 @@ const UserProfileTeams = ({
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="relative card-body-border-none card-padding-y-0"
|
className="relative card-body-border-none card-padding-y-0"
|
||||||
|
data-testid="user-team-card-container"
|
||||||
key="teams-card"
|
key="teams-card"
|
||||||
title={
|
title={
|
||||||
<Space align="center">
|
<Space align="center">
|
||||||
@ -75,30 +76,28 @@ const UserProfileTeams = ({
|
|||||||
<EditIcon
|
<EditIcon
|
||||||
className="cursor-pointer align-middle"
|
className="cursor-pointer align-middle"
|
||||||
color={DE_ACTIVE_COLOR}
|
color={DE_ACTIVE_COLOR}
|
||||||
data-testid="edit-teams"
|
data-testid="edit-teams-button"
|
||||||
{...ICON_DIMENSION}
|
{...ICON_DIMENSION}
|
||||||
onClick={() => setIsTeamsEdit(true)}
|
onClick={() => setIsTeamsEdit(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}>
|
}>
|
||||||
<div className="m-b-md">
|
{isTeamsEdit && isAdminUser ? (
|
||||||
{isTeamsEdit && isAdminUser ? (
|
<InlineEdit
|
||||||
<InlineEdit
|
direction="vertical"
|
||||||
direction="vertical"
|
onCancel={() => setIsTeamsEdit(false)}
|
||||||
onCancel={() => setIsTeamsEdit(false)}
|
onSave={handleTeamsSave}>
|
||||||
onSave={handleTeamsSave}>
|
<TeamsSelectable
|
||||||
<TeamsSelectable
|
filterJoinable
|
||||||
filterJoinable
|
maxValueCount={4}
|
||||||
maxValueCount={4}
|
selectedTeams={selectedTeams}
|
||||||
selectedTeams={selectedTeams}
|
onSelectionChange={setSelectedTeams}
|
||||||
onSelectionChange={setSelectedTeams}
|
/>
|
||||||
/>
|
</InlineEdit>
|
||||||
</InlineEdit>
|
) : (
|
||||||
) : (
|
teamsRenderElement
|
||||||
teamsRenderElement
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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 { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useAuth } from '../../../../hooks/authHooks';
|
||||||
|
import { USER_DATA, USER_TEAMS } from '../../../../mocks/User.mock';
|
||||||
|
import UserProfileTeams from './UserProfileTeams.component';
|
||||||
|
import { UserProfileTeamsProps } from './UserProfileTeams.interface';
|
||||||
|
|
||||||
|
const mockPropsData: UserProfileTeamsProps = {
|
||||||
|
teams: [],
|
||||||
|
updateUserDetails: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock('../../../../hooks/authHooks', () => ({
|
||||||
|
useAuth: jest.fn().mockReturnValue({ isAdminUser: true }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../../utils/CommonUtils', () => ({
|
||||||
|
getNonDeletedTeams: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../../components/InlineEdit/InlineEdit.component', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>InlineEdit</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../../common/Chip/Chip.component', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>Chip</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('../../../../components/TeamsSelectable/TeamsSelectable', () => {
|
||||||
|
return jest.fn().mockReturnValue(<p>TeamsSelectable</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Test User Profile Teams Component', () => {
|
||||||
|
it('should render user profile teams component', async () => {
|
||||||
|
render(<UserProfileTeams {...mockPropsData} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-team-card-container')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render teams if data available', async () => {
|
||||||
|
render(<UserProfileTeams {...mockPropsData} teams={USER_DATA.teams} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-team-card-container')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByTestId('edit-teams-button')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(await screen.findAllByText('Chip')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render teams select input on edit click', async () => {
|
||||||
|
render(<UserProfileTeams {...mockPropsData} teams={USER_DATA.teams} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-team-card-container')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const editButton = screen.getByTestId('edit-teams-button');
|
||||||
|
|
||||||
|
expect(editButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
|
expect(screen.getByText('InlineEdit')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render edit button to non admin user', async () => {
|
||||||
|
(useAuth as jest.Mock).mockImplementation(() => ({
|
||||||
|
isAdminUser: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<UserProfileTeams {...mockPropsData} teams={USER_TEAMS} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('user-team-card-container')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('edit-teams-button')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -10,10 +10,13 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Popover, Space, Tag, Typography } from 'antd';
|
import { Col, Popover, Row, Space, Tag, Typography } from 'antd';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { USER_DATA_SIZE } from '../../../constants/constants';
|
import {
|
||||||
|
NO_DATA_PLACEHOLDER,
|
||||||
|
USER_DATA_SIZE,
|
||||||
|
} from '../../../constants/constants';
|
||||||
import { EntityReference } from '../../../generated/entity/type';
|
import { EntityReference } from '../../../generated/entity/type';
|
||||||
import { getEntityName } from '../../../utils/EntityUtils';
|
import { getEntityName } from '../../../utils/EntityUtils';
|
||||||
import { ChipProps } from './Chip.interface';
|
import { ChipProps } from './Chip.interface';
|
||||||
@ -32,15 +35,15 @@ const Chip = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getChipElement = (item: EntityReference) => (
|
const getChipElement = (item: EntityReference) => (
|
||||||
<div
|
<Col
|
||||||
className="w-full d-flex items-center gap-2"
|
className="d-flex gap-1 items-center"
|
||||||
data-testid={item.name}
|
data-testid={item.name}
|
||||||
key={item.name}>
|
key={item.name}>
|
||||||
{icon}
|
{icon}
|
||||||
<Typography.Text className="w-56 text-left" ellipsis={{ tooltip: true }}>
|
<Typography.Text className="text-left">
|
||||||
{getEntityName(item)}
|
{getEntityName(item)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</Col>
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -49,14 +52,18 @@ const Chip = ({
|
|||||||
|
|
||||||
if (isEmpty(data) && showNoDataPlaceholder) {
|
if (isEmpty(data) && showNoDataPlaceholder) {
|
||||||
return (
|
return (
|
||||||
<Typography.Paragraph className="text-grey-muted">
|
<Typography.Paragraph className="text-grey-muted m-b-0">
|
||||||
{noDataPlaceholder}
|
{noDataPlaceholder ?? NO_DATA_PLACEHOLDER}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space wrap data-testid="chip-container" size={4}>
|
<Row
|
||||||
|
wrap
|
||||||
|
className="align-middle"
|
||||||
|
data-testid="chip-container"
|
||||||
|
gutter={[20, 6]}>
|
||||||
{data.slice(0, USER_DATA_SIZE).map(getChipElement)}
|
{data.slice(0, USER_DATA_SIZE).map(getChipElement)}
|
||||||
{hasMoreElement && (
|
{hasMoreElement && (
|
||||||
<Popover
|
<Popover
|
||||||
@ -73,7 +80,7 @@ const Chip = ({
|
|||||||
} more`}</Tag>
|
} more`}</Tag>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { EntityReference } from '../../../generated/entity/type';
|
|||||||
|
|
||||||
export interface ChipProps {
|
export interface ChipProps {
|
||||||
data: EntityReference[];
|
data: EntityReference[];
|
||||||
icon: React.ReactElement;
|
icon?: React.ReactElement;
|
||||||
noDataPlaceholder: string;
|
noDataPlaceholder?: string;
|
||||||
showNoDataPlaceholder?: boolean;
|
showNoDataPlaceholder?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,12 +49,12 @@ import mode from '../assets/img/service-icon-mode.png';
|
|||||||
import mongodb from '../assets/img/service-icon-mongodb.png';
|
import mongodb from '../assets/img/service-icon-mongodb.png';
|
||||||
import msAzure from '../assets/img/service-icon-ms-azure.png';
|
import msAzure from '../assets/img/service-icon-ms-azure.png';
|
||||||
import mssql from '../assets/img/service-icon-mssql.png';
|
import mssql from '../assets/img/service-icon-mssql.png';
|
||||||
|
import mstr from '../assets/img/service-icon-mstr.png';
|
||||||
import nifi from '../assets/img/service-icon-nifi.png';
|
import nifi from '../assets/img/service-icon-nifi.png';
|
||||||
import oracle from '../assets/img/service-icon-oracle.png';
|
import oracle from '../assets/img/service-icon-oracle.png';
|
||||||
import pinot from '../assets/img/service-icon-pinot.png';
|
import pinot from '../assets/img/service-icon-pinot.png';
|
||||||
import postgres from '../assets/img/service-icon-post.png';
|
import postgres from '../assets/img/service-icon-post.png';
|
||||||
import powerbi from '../assets/img/service-icon-power-bi.png';
|
import powerbi from '../assets/img/service-icon-power-bi.png';
|
||||||
import mstr from '../assets/img/service-icon-mstr.png';
|
|
||||||
import prefect from '../assets/img/service-icon-prefect.png';
|
import prefect from '../assets/img/service-icon-prefect.png';
|
||||||
import presto from '../assets/img/service-icon-presto.png';
|
import presto from '../assets/img/service-icon-presto.png';
|
||||||
import pulsar from '../assets/img/service-icon-pulsar.png';
|
import pulsar from '../assets/img/service-icon-pulsar.png';
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export const LOGGED_IN_USER_STORAGE_KEY = 'loggedInUsers';
|
|||||||
export const ACTIVE_DOMAIN_STORAGE_KEY = 'activeDomain';
|
export const ACTIVE_DOMAIN_STORAGE_KEY = 'activeDomain';
|
||||||
export const DEFAULT_DOMAIN_VALUE = 'All Domains';
|
export const DEFAULT_DOMAIN_VALUE = 'All Domains';
|
||||||
|
|
||||||
export const USER_DATA_SIZE = 4;
|
export const USER_DATA_SIZE = 5;
|
||||||
export const INITIAL_PAGING_VALUE = 1;
|
export const INITIAL_PAGING_VALUE = 1;
|
||||||
export const JSON_TAB_SIZE = 2;
|
export const JSON_TAB_SIZE = 2;
|
||||||
export const PAGE_SIZE = 10;
|
export const PAGE_SIZE = 10;
|
||||||
|
|||||||
142
openmetadata-ui/src/main/resources/ui/src/mocks/User.mock.ts
Normal file
142
openmetadata-ui/src/main/resources/ui/src/mocks/User.mock.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* 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 { User } from '../generated/entity/teams/user';
|
||||||
|
|
||||||
|
export const USER_DATA: User = {
|
||||||
|
id: '7f196a28-c4fa-4579-b420-f828985e7861',
|
||||||
|
name: 'admin',
|
||||||
|
fullyQualifiedName: 'admin',
|
||||||
|
description: '',
|
||||||
|
displayName: '',
|
||||||
|
version: 3.3,
|
||||||
|
updatedAt: 1698655259882,
|
||||||
|
updatedBy: 'admin',
|
||||||
|
email: 'admin@openmetadata.org',
|
||||||
|
href: 'http://localhost:8585/api/v1/users/7f196a28-c4fa-4579-b420-f828985e7861',
|
||||||
|
isBot: false,
|
||||||
|
isAdmin: true,
|
||||||
|
teams: [
|
||||||
|
{
|
||||||
|
id: '9e8b7464-3f3e-4071-af05-19be142d75db',
|
||||||
|
type: 'team',
|
||||||
|
name: 'Organization',
|
||||||
|
fullyQualifiedName: 'Organization',
|
||||||
|
description:
|
||||||
|
'Organization under which all the other team hierarchy is created',
|
||||||
|
displayName: 'Organization',
|
||||||
|
deleted: false,
|
||||||
|
href: 'http://localhost:8585/api/v1/teams/9e8b7464-3f3e-4071-af05-19be142d75db',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
personas: [
|
||||||
|
{
|
||||||
|
id: '0430976d-092a-46c9-90a8-61c6091a6f38',
|
||||||
|
type: 'persona',
|
||||||
|
name: 'Person-04',
|
||||||
|
fullyQualifiedName: 'Person-04',
|
||||||
|
description: 'Person-04',
|
||||||
|
displayName: 'Person-04',
|
||||||
|
href: 'http://localhost:8585/api/v1/personas/0430976d-092a-46c9-90a8-61c6091a6f38',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
changeDescription: {
|
||||||
|
fieldsAdded: [],
|
||||||
|
fieldsUpdated: [],
|
||||||
|
fieldsDeleted: [],
|
||||||
|
previousVersion: 3.2,
|
||||||
|
},
|
||||||
|
deleted: false,
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
id: 'ed94fd7c-0974-4b87-9295-02b36c4c6bcd',
|
||||||
|
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/ed94fd7c-0974-4b87-9295-02b36c4c6bcd',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'a24f61cc-be15-411a-aaf6-28a8c8029728',
|
||||||
|
type: 'role',
|
||||||
|
name: 'DataSteward',
|
||||||
|
fullyQualifiedName: 'DataSteward',
|
||||||
|
description: 'this is test description',
|
||||||
|
displayName: 'Data Steward',
|
||||||
|
deleted: false,
|
||||||
|
href: 'http://localhost:8585/api/v1/roles/a24f61cc-be15-411a-aaf6-28a8c8029728',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '257b6976-26c6-4397-b025-087b95e45788',
|
||||||
|
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/257b6976-26c6-4397-b025-087b95e45788',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inheritedRoles: [
|
||||||
|
{
|
||||||
|
id: 'ed94fd7c-0974-4b87-9295-02b36c4c6bcd',
|
||||||
|
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/ed94fd7c-0974-4b87-9295-02b36c4c6bcd',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isEmailVerified: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const USER_TEAMS = [
|
||||||
|
{
|
||||||
|
id: '9e8b7464-3f3e-4071-af05-19be142d75db',
|
||||||
|
type: 'team',
|
||||||
|
name: 'Organization',
|
||||||
|
fullyQualifiedName: 'Organization',
|
||||||
|
description:
|
||||||
|
'Organization under which all the other team hierarchy is created',
|
||||||
|
displayName: 'Organization',
|
||||||
|
deleted: false,
|
||||||
|
href: 'http://localhost:8585/api/v1/teams/9e8b7464-3f3e-4071-af05-19be142d75db',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: '9e8b7464-3f3e-4071-af05-19be142d75bc',
|
||||||
|
type: 'team',
|
||||||
|
name: 'Application',
|
||||||
|
fullyQualifiedName: 'Application',
|
||||||
|
description:
|
||||||
|
'Application under which all the other team hierarchy is created',
|
||||||
|
displayName: 'Application',
|
||||||
|
deleted: false,
|
||||||
|
href: 'http://localhost:8585/api/v1/teams/9e8b7464-3f3e-4071-af05-19be142d75bc',
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -745,3 +745,18 @@ a[href].link-text-grey,
|
|||||||
width: 36px;
|
width: 36px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collapse able headers
|
||||||
|
|
||||||
|
.header-collapse-custom-collapse .header-collapse-custom-panel {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
background: @user-profile-background;
|
||||||
|
border: 0px;
|
||||||
|
|
||||||
|
.ant-collapse-content {
|
||||||
|
.ant-collapse-content-box {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user