UI: Add support for User's profile image everywhere (#4856)

This commit is contained in:
darth-coder00 2022-05-17 11:20:49 +05:30 committed by GitHub
parent cd340a4d76
commit 5acb443d4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 565 additions and 78 deletions

View File

@ -11,15 +11,18 @@
* limitations under the License.
*/
import { isEmpty, isNil } from 'lodash';
import { isEmpty, isNil, isUndefined } from 'lodash';
import { action, makeAutoObservable } from 'mobx';
import { ClientAuth, NewUser, UserPermissions } from 'Models';
import { reactLocalStorage } from 'reactjs-localstorage';
import { LOCALSTORAGE_USER_PROFILES } from './constants/constants';
import { CurrentTourPageType } from './enums/tour.enum';
import { Role } from './generated/entity/teams/role';
import {
EntityReference as UserTeams,
User,
} from './generated/entity/teams/user';
import { ImageList } from './generated/type/profile';
class AppState {
users: Array<User> = [];
@ -36,6 +39,15 @@ class AppState {
userTeams: Array<UserTeams> = [];
userRoles: Array<Role> = [];
userPermissions: UserPermissions = {} as UserPermissions;
userProfilePics: Array<{
id: string;
name: string;
profile: ImageList['image512'];
}> = [];
userProfilePicsLoading: Array<{
id: string;
name: string;
}> = [];
inPageSearchText = '';
explorePageTab = 'tables';
@ -60,6 +72,13 @@ class AppState {
getAllTeams: action,
getAllRoles: action,
getAllPermissions: action,
getUserProfilePic: action,
updateUserProfilePic: action,
loadUserProfilePics: action,
getProfilePicsLoading: action,
updateProfilePicsLoading: action,
isProfilePicLoading: action,
removeProfilePicsLoading: action,
});
}
@ -96,6 +115,98 @@ class AppState {
this.explorePageTab = tab;
}
updateUserProfilePic(
id?: string,
username?: string,
profile?: ImageList['image512']
) {
if (!id && !username) {
return;
}
const filteredList = this.userProfilePics.filter((item) => {
// compare id only if present
if (item.id && id) {
return item.id !== id;
} else {
return item.name !== username;
}
});
this.userProfilePics = [
...filteredList,
{
id: id || '',
name: username || '',
profile,
},
];
reactLocalStorage.setObject(LOCALSTORAGE_USER_PROFILES, {
data: this.userProfilePics,
});
return profile;
}
updateProfilePicsLoading(id?: string, username?: string) {
if (!id && !username) {
return;
}
const alreadyLoading = !isUndefined(
this.userProfilePicsLoading.find((loadingItem) => {
// compare id only if present
if (loadingItem.id && id) {
return loadingItem.id === id;
} else {
return loadingItem.name === username;
}
})
);
if (!alreadyLoading) {
this.userProfilePicsLoading = [
...this.userProfilePicsLoading,
{
id: id || '',
name: username || '',
},
];
}
}
removeProfilePicsLoading(id?: string, username?: string) {
if (!id && !username) {
return;
}
const filteredList = this.userProfilePicsLoading.filter((loadingItem) => {
// compare id only if present
if (loadingItem.id && id) {
return loadingItem.id !== id;
} else {
return loadingItem.name !== username;
}
});
this.userProfilePicsLoading = filteredList;
}
loadUserProfilePics() {
const { data } = reactLocalStorage.getObject(
LOCALSTORAGE_USER_PROFILES
) as {
data: Array<{
id: string;
name: string;
profile: ImageList['image512'];
}>;
};
if (data) {
this.userProfilePics = data;
}
}
getCurrentUserDetails() {
if (!isEmpty(this.userDetails) && !isNil(this.userDetails)) {
return this.userDetails;
@ -109,6 +220,40 @@ class AppState {
}
}
getUserProfilePic(id?: string, username?: string) {
const data = this.userProfilePics.find((item) => {
// compare id only if present
if (item.id && id) {
return item.id === id;
} else {
return item.name === username;
}
});
return data?.profile;
}
getAllUserProfilePics() {
return this.userProfilePics;
}
getProfilePicsLoading() {
return this.userProfilePicsLoading;
}
isProfilePicLoading(id?: string, username?: string) {
const data = this.userProfilePicsLoading.find((loadingPic) => {
// compare id only if present
if (loadingPic.id && id) {
return loadingPic.id === id;
} else {
return loadingPic.name === username;
}
});
return Boolean(data);
}
getAllUsers() {
return this.users;
}

View File

@ -64,6 +64,15 @@ export const getUserByName = (
return APIClient.get(url);
};
export const getUserById = (
id: string,
arrQueryFields?: string
): Promise<AxiosResponse> => {
const url = getURLWithQueryFields(`/users/${id}`, arrQueryFields);
return APIClient.get(url);
};
export const getLoggedInUser = (arrQueryFields?: string) => {
const url = getURLWithQueryFields('/users/loggedInUser', arrQueryFields);
@ -98,10 +107,6 @@ export const updateUserTeam: Function = (
return APIClient.post(`/users/${id}/teams`, options);
};
export const getUserById: Function = (id: string): Promise<AxiosResponse> => {
return APIClient.get(`/users/${id}`);
};
export const createUser = (
userDetails: Record<string, string | Array<string> | UserProfile> | CreateUser
): Promise<AxiosResponse> => {

View File

@ -30,8 +30,10 @@ jest.mock('../../../utils/TimeUtils', () => ({
getDayTimeByTimeStamp: jest.fn(),
}));
jest.mock('../../common/avatar/Avatar', () => {
return jest.fn().mockReturnValue(<p data-testid="replied-user">Avatar</p>);
jest.mock('../../common/ProfilePicture/ProfilePicture', () => {
return jest
.fn()
.mockReturnValue(<p data-testid="replied-user">ProfilePicture</p>);
});
const mockFeedCardFooterPorps = {

View File

@ -15,7 +15,7 @@ import { isUndefined, toLower } from 'lodash';
import React, { FC } from 'react';
import { getReplyText } from '../../../utils/FeedUtils';
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
import Avatar from '../../common/avatar/Avatar';
import ProfilePicture from '../../common/ProfilePicture/ProfilePicture';
import { FeedFooterProp } from '../ActivityFeedCard/ActivityFeedCard.interface';
const FeedCardFooter: FC<FeedFooterProp> = ({
@ -36,12 +36,12 @@ const FeedCardFooter: FC<FeedFooterProp> = ({
isFooterVisible ? (
<div className="tw-flex tw-group">
{repliedUsers?.map((u, i) => (
<Avatar
<ProfilePicture
className="tw-mt-0.5 tw-mx-0.5"
data-testid="replied-user"
id=""
key={i}
name={u}
type="square"
width="22"
/>
))}

View File

@ -39,8 +39,8 @@ jest.mock('../../../utils/TimeUtils', () => ({
getDayTimeByTimeStamp: jest.fn(),
}));
jest.mock('../../common/avatar/Avatar', () => {
return jest.fn().mockReturnValue(<p>Avatar</p>);
jest.mock('../../common/ProfilePicture/ProfilePicture', () => {
return jest.fn().mockReturnValue(<p>ProfilePicture</p>);
});
const mockFeedHeaderProps = {

View File

@ -38,8 +38,8 @@ import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { getEntityLink } from '../../../utils/TableUtils';
import { getDayTimeByTimeStamp } from '../../../utils/TimeUtils';
import { Button } from '../../buttons/Button/Button';
import Avatar from '../../common/avatar/Avatar';
import PopOver from '../../common/popover/PopOver';
import ProfilePicture from '../../common/ProfilePicture/ProfilePicture';
import Loader from '../../Loader/Loader';
import { FeedHeaderProp } from '../ActivityFeedCard/ActivityFeedCard.interface';
import './FeedCardHeader.style.css';
@ -92,7 +92,7 @@ const FeedCardHeader: FC<FeedHeaderProp> = ({
<div>
<div className="tw-flex">
<div className="tw-mr-2">
<Avatar name={createdBy} type="square" width="30" />
<ProfilePicture id="" name={createdBy} width="30" />
</div>
<div className="tw-self-center">
<Button
@ -256,7 +256,7 @@ const FeedCardHeader: FC<FeedHeaderProp> = ({
className="tw-cursor-pointer"
data-testid="authorAvatar"
onClick={onClickHandler}>
<Avatar name={createdBy} type="square" width="30" />
<ProfilePicture id="" name={createdBy} width="30" />
</span>
</PopOver>
<h6 className="tw-flex tw-items-center tw-m-0 tw-heading tw-pl-2">

View File

@ -13,7 +13,7 @@
import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { EntityTags, TagOption } from 'Models';
import { EntityTags, ExtraInfo, TagOption } from 'Models';
import React, { RefObject, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
@ -21,6 +21,7 @@ import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { getTeamAndUserDetailsPath } from '../../constants/constants';
import { observerOptions } from '../../constants/Mydata.constants';
import { EntityType } from '../../enums/entity.enum';
import { OwnerType } from '../../enums/user.enum';
import { Dashboard } from '../../generated/entity/data/dashboard';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { EntityReference } from '../../generated/type/entityReference';
@ -197,7 +198,7 @@ const DashboardDetails = ({
},
];
const extraInfo = [
const extraInfo: Array<ExtraInfo> = [
{
key: 'Owner',
value:
@ -210,6 +211,7 @@ const DashboardDetails = ({
),
isLink: owner?.type === 'team',
openInNewTab: false,
profileName: owner?.type === OwnerType.USER ? owner?.name : undefined,
},
{
key: 'Tier',

View File

@ -17,6 +17,7 @@ import { ExtraInfo } from 'Models';
import React, { FC, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { OwnerType } from '../../enums/user.enum';
import { ChangeDescription } from '../../generated/entity/data/dashboard';
import { TagLabel } from '../../generated/type/tagLabel';
import { isEven } from '../../utils/CommonUtils';
@ -141,6 +142,8 @@ const DashboardVersion: FC<DashboardVersionProp> = ({
: ownerPlaceHolder
? getDiffValue(ownerPlaceHolder, ownerPlaceHolder)
: '',
profileName:
newOwner?.type === OwnerType.USER ? newOwner?.name : undefined,
},
{
key: 'Tier',

View File

@ -21,6 +21,7 @@ import { getTeamAndUserDetailsPath, ROUTES } from '../../constants/constants';
import { observerOptions } from '../../constants/Mydata.constants';
import { CSMode } from '../../enums/codemirror.enum';
import { EntityType, FqnPart } from '../../enums/entity.enum';
import { OwnerType } from '../../enums/user.enum';
import {
JoinedWith,
Table,
@ -371,6 +372,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
),
isLink: owner?.type === 'team',
openInNewTab: false,
profileName: owner?.type === OwnerType.USER ? owner?.name : undefined,
},
{
key: 'Tier',

View File

@ -17,6 +17,7 @@ import { ExtraInfo } from 'Models';
import React, { useEffect, useState } from 'react';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { FqnPart } from '../../enums/entity.enum';
import { OwnerType } from '../../enums/user.enum';
import {
ChangeDescription,
Column,
@ -120,6 +121,8 @@ const DatasetVersion: React.FC<DatasetVersionProp> = ({
: ownerPlaceHolder
? getDiffValue(ownerPlaceHolder, ownerPlaceHolder)
: '',
profileName:
newOwner?.type === OwnerType.USER ? newOwner?.name : undefined,
},
{
key: 'Tier',

View File

@ -35,9 +35,9 @@ import {
} from '../../utils/TagsUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { Button } from '../buttons/Button/Button';
import Avatar from '../common/avatar/Avatar';
import Description from '../common/description/Description';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
import TabsPane from '../common/TabsPane/TabsPane';
import ManageTabComponent from '../ManageTab/ManageTab.component';
import ReviewerModal from '../Modals/ReviewerModal/ReviewerModal.component';
@ -277,8 +277,10 @@ const GlossaryDetails = ({
<div className="tw-mb-3 tw-flex tw-items-center">
{glossary.owner && getEntityName(glossary.owner) && (
<div className="tw-inline-block tw-mr-2">
<Avatar
name={getEntityName(glossary.owner)}
<ProfilePicture
displayName={getEntityName(glossary.owner)}
id={glossary.owner?.id || ''}
name={glossary.owner?.name || ''}
textClass="tw-text-xs"
width="20"
/>

View File

@ -55,6 +55,10 @@ jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviewer</p>);
});
jest.mock('../common/ProfilePicture/ProfilePicture', () => {
return jest.fn().mockReturnValue(<p>ProfilePicture</p>);
});
const mockProps = {
glossary: mockedGlossaries[0],
isHasAccess: true,

View File

@ -98,9 +98,10 @@ const RelatedTermsModal = ({
isCheckBoxes
item={{
name: '',
description: d.displayName || d.name,
displayName: d.displayName || d.name,
id: d.id,
isChecked: isIncludeInOptions(d.id),
type: d.type,
}}
key={d.id}
onSelect={selectionHandler}

View File

@ -116,9 +116,11 @@ const ReviewerModal = ({
isIconVisible
item={{
name: d.name,
description: d.displayName,
displayName: d.displayName || d.name,
email: d.email,
id: d.id,
isChecked: isIncludeInOptions(d.id),
type: d.type,
}}
key={d.id}
onSelect={selectionHandler}

View File

@ -14,7 +14,7 @@
import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { isNil } from 'lodash';
import { EntityFieldThreads, EntityTags } from 'Models';
import { EntityFieldThreads, EntityTags, ExtraInfo } from 'Models';
import React, { Fragment, RefObject, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
@ -22,6 +22,7 @@ import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { getTeamAndUserDetailsPath } from '../../constants/constants';
import { observerOptions } from '../../constants/Mydata.constants';
import { EntityType } from '../../enums/entity.enum';
import { OwnerType } from '../../enums/user.enum';
import { Pipeline, Task } from '../../generated/entity/data/pipeline';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { EntityReference } from '../../generated/type/entityReference';
@ -202,7 +203,7 @@ const PipelineDetails = ({
},
];
const extraInfo = [
const extraInfo: Array<ExtraInfo> = [
{
key: 'Owner',
value:
@ -215,6 +216,7 @@ const PipelineDetails = ({
),
isLink: owner?.type === 'team',
openInNewTab: false,
profileName: owner?.type === OwnerType.USER ? owner?.name : undefined,
},
{
key: 'Tier',

View File

@ -17,6 +17,7 @@ import { ExtraInfo } from 'Models';
import React, { FC, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { OwnerType } from '../../enums/user.enum';
import { ChangeDescription } from '../../generated/entity/data/pipeline';
import { TagLabel } from '../../generated/type/tagLabel';
import { isEven } from '../../utils/CommonUtils';
@ -141,6 +142,8 @@ const PipelineVersion: FC<PipelineVersionProp> = ({
: ownerPlaceHolder
? getDiffValue(ownerPlaceHolder, ownerPlaceHolder)
: '',
profileName:
newOwner?.type === OwnerType.USER ? newOwner?.name : undefined,
},
{
key: 'Tier',

View File

@ -151,6 +151,10 @@ const TeamDetails = ({
currentTeam?.owner?.displayName || currentTeam?.owner?.name || '',
isLink: currentTeam?.owner?.type === 'team',
openInNewTab: false,
profileName:
currentTeam?.owner?.type === OwnerType.USER
? currentTeam?.owner?.name
: undefined,
};
const isActionAllowed = (operation = false) => {

View File

@ -11,13 +11,14 @@
* limitations under the License.
*/
import { EntityTags } from 'Models';
import { EntityTags, ExtraInfo } from 'Models';
import React, { Fragment, RefObject, useEffect, useState } from 'react';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { getTeamAndUserDetailsPath } from '../../constants/constants';
import { observerOptions } from '../../constants/Mydata.constants';
import { EntityType } from '../../enums/entity.enum';
import { OwnerType } from '../../enums/user.enum';
import { Topic } from '../../generated/entity/data/topic';
import { EntityReference } from '../../generated/type/entityReference';
import { Paging } from '../../generated/type/paging';
@ -207,7 +208,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
position: 5,
},
];
const extraInfo = [
const extraInfo: Array<ExtraInfo> = [
{
key: 'Owner',
value:
@ -220,6 +221,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
),
isLink: owner?.type === 'team',
openInNewTab: false,
profileName: owner?.type === OwnerType.USER ? owner?.name : undefined,
},
{
key: 'Tier',

View File

@ -16,6 +16,7 @@ import { isUndefined } from 'lodash';
import { ExtraInfo } from 'Models';
import React, { FC, useEffect, useState } from 'react';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { OwnerType } from '../../enums/user.enum';
import { ChangeDescription } from '../../generated/entity/data/topic';
import { TagLabel } from '../../generated/type/tagLabel';
import {
@ -172,6 +173,8 @@ const TopicVersion: FC<TopicVersionProp> = ({
: ownerPlaceHolder
? getDiffValue(ownerPlaceHolder, ownerPlaceHolder)
: '',
profileName:
newOwner?.type === OwnerType.USER ? newOwner?.name : undefined,
},
{
key: 'Tier',

View File

@ -17,7 +17,7 @@ import { MemoryRouter } from 'react-router-dom';
import UserDataCard from './UserDataCard';
const mockItem = {
description: 'description1',
displayName: 'description1',
name: 'name1',
id: 'id1',
email: 'string@email.com',
@ -41,8 +41,10 @@ jest.mock('../../authentication/auth-provider/AuthProvider', () => {
};
});
jest.mock('../../components/common/avatar/Avatar', () => {
return jest.fn().mockReturnValue(<p data-testid="avatar">Avatar</p>);
jest.mock('../../components/common/ProfilePicture/ProfilePicture', () => {
return jest
.fn()
.mockReturnValue(<p data-testid="profile-picture">ProfilePicture</p>);
});
jest.mock('../../utils/SvgUtils', () => {
@ -69,7 +71,7 @@ describe('Test UserDataCard component', () => {
);
const cardContainer = await findByTestId(container, 'user-card-container');
const avatar = await findByTestId(container, 'avatar');
const avatar = await findByTestId(container, 'profile-picture');
expect(avatar).toBeInTheDocument();
expect(cardContainer).toBeInTheDocument();

View File

@ -15,11 +15,11 @@ import classNames from 'classnames';
import { isNil } from 'lodash';
import React from 'react';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import Avatar from '../common/avatar/Avatar';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
type Item = {
description: string;
displayName: string;
name: string;
id?: string;
email: string;
@ -41,13 +41,11 @@ const UserDataCard = ({ item, onClick, onDelete, showTeams = true }: Props) => {
className="tw-card tw-flex tw-justify-between tw-py-2 tw-px-3 tw-group"
data-testid="user-card-container">
<div className="tw-flex tw-gap-1">
{item.profilePhoto ? (
<div className="tw-h-9 tw-w-9">
<img alt="profile" className="tw-w-full" src={item.profilePhoto} />
</div>
) : (
<Avatar name={item.description} />
)}
<ProfilePicture
displayName={item?.displayName}
id={item?.id || ''}
name={item?.name || ''}
/>
<div
className="tw-flex tw-flex-col tw-flex-1 tw-pl-2"
@ -60,7 +58,7 @@ const UserDataCard = ({ item, onClick, onDelete, showTeams = true }: Props) => {
onClick={() => {
onClick?.(item.name);
}}>
{item.description}
{item.displayName}
</p>
{!item?.isActiveUser && (
<span className="tw-text-xs tw-bg-badge tw-border tw-px-2 tw-py-0.5 tw-rounded">
@ -83,7 +81,7 @@ const UserDataCard = ({ item, onClick, onDelete, showTeams = true }: Props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onDelete(item.id as string, item.description);
onDelete(item.id as string, item.displayName);
}}>
<SVGIcons
alt="delete"

View File

@ -107,7 +107,7 @@ const UserDetails = ({
data-testid="user-card-container">
{selectedUserList.map((user, index) => {
const User = {
description: getEntityName(user as unknown as EntityReference),
displayName: getEntityName(user as unknown as EntityReference),
name: user.name || '',
id: user.id,
email: user.email || '',

View File

@ -347,7 +347,7 @@ const UserList: FunctionComponent<Props> = ({
data-testid="user-card-container">
{listUserData.map((user, index) => {
const User = {
description: getEntityName(user as unknown as EntityReference),
displayName: getEntityName(user as unknown as EntityReference),
name: user.name || '',
id: user.id,
email: user.email || '',

View File

@ -109,8 +109,8 @@ const mockUserData = {
],
};
jest.mock('../common/avatar/Avatar', () => {
return jest.fn().mockReturnValue(<p>Avatar</p>);
jest.mock('../common/ProfilePicture/ProfilePicture', () => {
return jest.fn().mockReturnValue(<p>ProfilePicture</p>);
});
jest.mock('../../pages/teams/UserCard', () => {

View File

@ -45,8 +45,8 @@ import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList';
import { Button } from '../buttons/Button/Button';
import Avatar from '../common/avatar/Avatar';
import Description from '../common/description/Description';
import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
import { reactSingleSelectCustomStyle } from '../common/react-select-component/reactSelectCustomStyle';
import TabsPane from '../common/TabsPane/TabsPane';
import PageLayout from '../containers/PageLayout';
@ -525,12 +525,15 @@ const Users = ({
<img
alt="profile"
className="tw-w-full"
referrerPolicy="no-referrer"
src={userData.profile?.images?.image}
/>
</div>
) : (
<Avatar
name={userData?.displayName || userData.name}
<ProfilePicture
displayName={userData?.displayName || userData.name}
id={userData?.id || ''}
name={userData?.name || ''}
textClass="tw-text-5xl"
width="112"
/>

View File

@ -0,0 +1,78 @@
/*
* Copyright 2021 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 { findByTestId, findByText, render } from '@testing-library/react';
import React from 'react';
import AppState from '../../../AppState';
import { getUserProfilePic } from '../../../utils/UserDataUtils';
import ProfilePicture from './ProfilePicture';
jest.mock('../avatar/Avatar', () => {
return jest.fn().mockImplementation(() => <div>Avatar</div>);
});
jest.mock('../../../utils/UserDataUtils', () => {
return {
fetchAllUsers: jest.fn(),
fetchUserProfilePic: jest.fn(),
getUserDataFromOidc: jest.fn(),
getUserProfilePic: jest.fn(),
matchUserDetails: jest.fn(),
};
});
const mockData = {
id: 'test-1',
name: 'test-name',
};
const mockGetUserProfilePic = jest.fn(() => 'mockedProfilePic');
beforeAll(() => {
jest.spyOn(AppState, 'isProfilePicLoading').mockImplementation(() => false);
});
afterAll(() => {
jest.restoreAllMocks();
});
describe('Test ProfilePicture component', () => {
it('ProfilePicture component should render with Avatar', async () => {
const { container } = render(<ProfilePicture {...mockData} />);
const avatar = await findByText(container, 'Avatar');
expect(avatar).toBeInTheDocument();
});
it('Profile image should load', async () => {
(getUserProfilePic as jest.Mock).mockImplementationOnce(
mockGetUserProfilePic
);
const { container } = render(<ProfilePicture {...mockData} />);
const image = await findByTestId(container, 'profile-image');
expect(image).toBeInTheDocument();
});
it('Profile Avatar should be loading', async () => {
jest.spyOn(AppState, 'isProfilePicLoading').mockImplementation(() => true);
const { container } = render(<ProfilePicture {...mockData} />);
const loader = await findByTestId(container, 'loader-cntnr');
expect(loader).toBeInTheDocument();
});
});

View File

@ -0,0 +1,101 @@
/*
* Copyright 2021 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 classNames from 'classnames';
import { observer } from 'mobx-react';
import { ImageShape } from 'Models';
import React, { useMemo } from 'react';
import AppState from '../../../AppState';
import { EntityReference, User } from '../../../generated/entity/teams/user';
import { getEntityName } from '../../../utils/CommonUtils';
import { getUserProfilePic } from '../../../utils/UserDataUtils';
import Loader from '../../Loader/Loader';
import Avatar from '../avatar/Avatar';
type UserData = Pick<User, 'id' | 'name' | 'displayName'>;
interface Props extends UserData {
width?: string;
type?: ImageShape;
textClass?: string;
className?: string;
}
const ProfilePicture = ({
id,
name,
displayName,
className = '',
textClass = '',
type = 'square',
width = '36',
}: Props) => {
const profilePic = useMemo(() => {
return getUserProfilePic(id, name);
}, [id, name, AppState.userProfilePics]);
const isPicLoading = useMemo(() => {
return AppState.isProfilePicLoading(id, name);
}, [id, name, AppState.userProfilePicsLoading]);
const getAvatarByName = () => {
return (
<Avatar
className={className}
name={getEntityName({ name, displayName } as EntityReference)}
textClass={textClass}
type={type}
width={width}
/>
);
};
const getAvatarElement = () => {
return isPicLoading ? (
<div
className="tw-inline-block tw-relative"
style={{ height: `${width}px`, width: `${width}px` }}>
{getAvatarByName()}
<div
className="tw-absolute tw-inset-0 tw-opacity-60 tw-bg-grey-backdrop tw-rounded"
data-testid="loader-cntnr">
<Loader
className="tw-absolute tw-inset-0"
size="small"
style={{ height: `${+width - 2}px`, width: `${+width - 2}px` }}
type="white"
/>
</div>
</div>
) : (
getAvatarByName()
);
};
return profilePic ? (
<div
className={classNames('profile-image', type)}
style={{ height: `${width}px`, width: `${width}px` }}>
<img
alt="user"
data-testid="profile-image"
referrerPolicy="no-referrer"
src={profilePic}
/>
</div>
) : (
getAvatarElement()
);
};
export default observer(ProfilePicture);

View File

@ -12,6 +12,7 @@
*/
import classNames from 'classnames';
import { ImageShape } from 'Models';
import React from 'react';
import { getRandomColor } from '../../../utils/CommonUtils';
@ -26,7 +27,7 @@ const Avatar = ({
width?: string;
textClass?: string;
className?: string;
type?: 'circle' | 'square';
type?: ImageShape;
}) => {
const { color, character } = getRandomColor(name);

View File

@ -153,8 +153,8 @@ jest.mock('../../tags/tags', () => {
return jest.fn().mockReturnValue(<p data-testid="tier-tag">Tag</p>);
});
jest.mock('../avatar/Avatar', () => {
return jest.fn().mockReturnValue(<p>Avatar</p>);
jest.mock('../ProfilePicture/ProfilePicture', () => {
return jest.fn().mockReturnValue(<p>ProfilePicture</p>);
});
jest.mock('./FollowersModal', () => {

View File

@ -34,9 +34,9 @@ import { getTagCategories, getTaglist } from '../../../utils/TagsUtils';
import TagsContainer from '../../tags-container/tags-container';
import TagsViewer from '../../tags-viewer/tags-viewer';
import Tags from '../../tags/tags';
import Avatar from '../avatar/Avatar';
import NonAdminAction from '../non-admin-action/NonAdminAction';
import PopOver from '../popover/PopOver';
import ProfilePicture from '../ProfilePicture/ProfilePicture';
import TitleBreadcrumb from '../title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from '../title-breadcrumb/title-breadcrumb.interface';
import FollowersModal from './FollowersModal';
@ -157,8 +157,10 @@ const EntityPageInfo = ({
})}>
{list.slice(0, FOLLOWERS_VIEW_CAP).map((follower, index) => (
<div className="tw-flex" key={index}>
<Avatar
name={(follower?.displayName || follower?.name) as string}
<ProfilePicture
displayName={follower?.displayName || follower?.name}
id={follower?.id || ''}
name={follower?.name || ''}
width="20"
/>
<span className="tw-self-center tw-ml-2">

View File

@ -22,8 +22,11 @@ import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { ROUTES } from '../../../constants/constants';
import { SearchIndex } from '../../../enums/search.enum';
import { CurrentTourPageType } from '../../../enums/tour.enum';
import { OwnerType } from '../../../enums/user.enum';
import { TableType } from '../../../generated/entity/data/table';
import { EntityReference } from '../../../generated/type/entityReference';
import { TagLabel } from '../../../generated/type/tagLabel';
import { getEntityName } from '../../../utils/CommonUtils';
import { serviceTypeLogo } from '../../../utils/ServiceUtils';
import { stringToHTML } from '../../../utils/StringsUtils';
import { getEntityLink, getUsagePercentile } from '../../../utils/TableUtils';
@ -32,7 +35,7 @@ import TableDataCardBody from './TableDataCardBody';
type Props = {
name: string;
owner?: string;
owner?: EntityReference;
description?: string;
tableType?: TableType;
id?: string;
@ -53,7 +56,7 @@ type Props = {
};
const TableDataCard: FunctionComponent<Props> = ({
owner = '',
owner,
description,
id,
tier = '',
@ -80,7 +83,12 @@ const TableDataCard: FunctionComponent<Props> = ({
};
const OtherDetails: Array<ExtraInfo> = [
{ key: 'Owner', value: owner, avatarWidth: '16' },
{
key: 'Owner',
value: getEntityName(owner),
avatarWidth: '16',
profileName: owner?.type === OwnerType.USER ? owner?.name : undefined,
},
{ key: 'Tier', value: getTier() },
];
if (indexType !== SearchIndex.DASHBOARD && usage !== undefined) {

View File

@ -177,9 +177,10 @@ const NavBar = ({
title="Profile"
trigger="mouseenter">
{AppState?.userDetails?.profile?.images?.image512 ? (
<div className="profile-image tw--mr-2">
<div className="profile-image square tw--mr-2">
<img
alt="user"
referrerPolicy="no-referrer"
src={AppState.userDetails.profile.images.image512}
/>
</div>

View File

@ -112,7 +112,7 @@ const SearchedData: React.FC<SearchedDataProp> = ({
indexType={table.index}
matches={matches}
name={name}
owner={table.owner?.displayName || table.owner?.name}
owner={table.owner}
service={table.service}
serviceType={table.serviceType || '--'}
tableType={table.tableType as TableType}

View File

@ -36,6 +36,16 @@ const tagListWithTier = [
const onCancel = jest.fn();
const onSelectionChange = jest.fn();
jest.mock('../../utils/UserDataUtils', () => {
return {
fetchAllUsers: jest.fn(),
fetchUserProfilePic: jest.fn(),
getUserDataFromOidc: jest.fn(),
getUserProfilePic: jest.fn(),
matchUserDetails: jest.fn(),
};
});
jest.mock('../tags/tags', () => {
return jest.fn().mockReturnValue(<p>tags</p>);
});

View File

@ -31,6 +31,7 @@ export const INGESTION_PROGRESS_END_VAL = 80;
export const DEPLOYED_PROGRESS_VAL = 100;
export const LOCALSTORAGE_RECENTLY_VIEWED = `recentlyViewedData_${COOKIE_VERSION}`;
export const LOCALSTORAGE_RECENTLY_SEARCHED = `recentlySearchedData_${COOKIE_VERSION}`;
export const LOCALSTORAGE_USER_PROFILES = 'userProfiles';
export const oidcTokenKey = 'oidcIdToken';
export const TERM_ADMIN = 'Admin';
export const TERM_USER = 'User';

View File

@ -495,6 +495,7 @@ declare module 'Models' {
openInNewTab?: boolean;
showLabel?: boolean;
avatarWidth?: string;
profileName?: string;
};
export type TourSteps = {
@ -603,4 +604,6 @@ declare module 'Models' {
count: number;
entityField: string;
}
export type ImageShape = 'circle' | 'square';
}

View File

@ -64,6 +64,7 @@ import {
import { observerOptions } from '../../constants/Mydata.constants';
import { EntityType, FqnPart, TabSpecificField } from '../../enums/entity.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { OwnerType } from '../../enums/user.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
import { DatabaseSchema } from '../../generated/entity/data/databaseSchema';
import { Table } from '../../generated/entity/data/table';
@ -189,6 +190,10 @@ const DatabaseSchemaPage: FunctionComponent = () => {
databaseSchema?.owner?.displayName || databaseSchema?.owner?.name || '',
isLink: databaseSchema?.owner?.type === 'team',
openInNewTab: false,
profileName:
databaseSchema?.owner?.type === OwnerType.USER
? databaseSchema?.owner?.name
: undefined,
},
];

View File

@ -64,6 +64,7 @@ import {
import { observerOptions } from '../../constants/Mydata.constants';
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { OwnerType } from '../../enums/user.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
import { Database } from '../../generated/entity/data/database';
import { DatabaseSchema } from '../../generated/entity/data/databaseSchema';
@ -194,6 +195,10 @@ const DatabaseDetails: FunctionComponent = () => {
database?.owner?.displayName || database?.owner?.name || '',
isLink: database?.owner?.type === 'team',
openInNewTab: false,
profileName:
database?.owner?.type === OwnerType.USER
? database?.owner?.name
: undefined,
},
];

View File

@ -53,6 +53,7 @@ import {
} from '../../constants/constants';
import { SearchIndex } from '../../enums/search.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { OwnerType } from '../../enums/user.enum';
import { Dashboard } from '../../generated/entity/data/dashboard';
import { Database } from '../../generated/entity/data/database';
import { Pipeline } from '../../generated/entity/data/pipeline';
@ -193,6 +194,10 @@ const ServicePage: FunctionComponent = () => {
placeholderText: serviceDetails?.owner?.displayName || '',
isLink: serviceDetails?.owner?.type === 'team',
openInNewTab: false,
profileName:
serviceDetails?.owner?.type === OwnerType.USER
? serviceDetails?.owner?.name
: undefined,
},
];

View File

@ -13,12 +13,16 @@
import classNames from 'classnames';
import { capitalize } from 'lodash';
import { FormattedUsersData } from 'Models';
import React, { useState } from 'react';
import Avatar from '../../components/common/avatar/Avatar';
import ProfilePicture from '../../components/common/ProfilePicture/ProfilePicture';
import SVGIcons from '../../utils/SvgUtils';
type Props = {
item: { description: string; name: string; id: string; isChecked: boolean };
item: Pick<FormattedUsersData, 'displayName' | 'id' | 'name' | 'type'> & {
email?: string;
isChecked: boolean;
};
isActionVisible?: boolean;
isIconVisible?: boolean;
isCheckBoxes?: boolean;
@ -44,7 +48,11 @@ const CheckboxUserCard = ({
data-testid="user-card-container">
{isIconVisible && (
<div className="tw-flex tw-mr-2">
<Avatar name={item.description || item.name} />
<ProfilePicture
displayName={item.displayName || item.name}
id={item.id || ''}
name={item.name || ''}
/>
</div>
)}
<div
@ -56,8 +64,8 @@ const CheckboxUserCard = ({
'tw-font-normal',
isActionVisible ? 'tw-truncate tw-w-32' : null
)}
title={item.description}>
{item.description}
title={item.displayName}>
{item.displayName}
</p>
{item.name && (
<p

View File

@ -37,8 +37,10 @@ jest.mock('../../authentication/auth-provider/AuthProvider', () => {
};
});
jest.mock('../../components/common/avatar/Avatar', () => {
return jest.fn().mockReturnValue(<p data-testid="avatar">Avatar</p>);
jest.mock('../../components/common/ProfilePicture/ProfilePicture', () => {
return jest
.fn()
.mockReturnValue(<p data-testid="profile-picture">ProfilePicture</p>);
});
jest.mock('../../utils/SvgUtils', () => {
@ -60,7 +62,7 @@ describe('Test userCard component', () => {
});
const cardContainer = await findByTestId(container, 'user-card-container');
const avatar = await findByTestId(container, 'avatar');
const avatar = await findByTestId(container, 'profile-picture');
expect(avatar).toBeInTheDocument();
expect(cardContainer).toBeInTheDocument();

View File

@ -17,8 +17,8 @@ import { capitalize } from 'lodash';
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import Avatar from '../../components/common/avatar/Avatar';
import NonAdminAction from '../../components/common/non-admin-action/NonAdminAction';
import ProfilePicture from '../../components/common/ProfilePicture/ProfilePicture';
import { AssetsType, FqnPart } from '../../enums/entity.enum';
import { SearchIndex } from '../../enums/search.enum';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
@ -157,7 +157,11 @@ const UserCard = ({
data-testid="user-card-container">
<div className={`tw-flex ${isCheckBoxes ? 'tw-mr-2' : 'tw-gap-1'}`}>
{isIconVisible && !isDataset ? (
<Avatar name={item.displayName} />
<ProfilePicture
displayName={item.displayName || item.name}
id={item.id || ''}
name={item.name || ''}
/>
) : (
<Fragment>{getDatasetIcon(item.type)}</Fragment>
)}

View File

@ -43,6 +43,7 @@ import {
getTeamAndUserDetailsPath,
TITLE_FOR_NON_ADMIN_ACTION,
} from '../../constants/constants';
import { OwnerType } from '../../enums/user.enum';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { Team } from '../../generated/entity/teams/team';
import {
@ -103,6 +104,10 @@ const TeamsPage = () => {
currentTeam?.owner?.displayName || currentTeam?.owner?.name || '',
isLink: currentTeam?.owner?.type === 'team',
openInNewTab: false,
profileName:
currentTeam?.owner?.type === OwnerType.USER
? currentTeam?.owner?.name
: undefined,
},
];

View File

@ -24,10 +24,19 @@
.profile-image svg,
.profile-image img {
border-radius: 50%;
display: inline-block;
}
.profile-image.circle svg,
.profile-image.circle img {
border-radius: 50%;
}
.profile-image.square svg,
.profile-image.square img {
border-radius: 4px;
}
.teams-dropdown {
height: 36px;
background-color: transparent !important;

View File

@ -622,7 +622,7 @@ export const getEntityPlaceHolder = (value: string, isDeleted?: boolean) => {
* @param entity - entity reference
* @returns - entity name
*/
export const getEntityName = (entity: EntityReference) => {
export const getEntityName = (entity?: EntityReference) => {
return entity?.displayName || entity?.name || '';
};

View File

@ -15,7 +15,7 @@ import classNames from 'classnames';
import { isEmpty, isNil, isString, isUndefined, startCase } from 'lodash';
import { Bucket, ExtraInfo, LeafNodes, LineagePos } from 'Models';
import React from 'react';
import Avatar from '../components/common/avatar/Avatar';
import ProfilePicture from '../components/common/ProfilePicture/ProfilePicture';
import TableProfilerGraph from '../components/TableProfiler/TableProfilerGraph.component';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import {
@ -382,9 +382,10 @@ export const getInfoElements = (data: ExtraInfo) => {
displayVal && displayVal !== '--' ? (
isString(displayVal) ? (
<div className="tw-inline-block tw-mr-2">
<Avatar
name={displayVal}
textClass="tw-text-xs"
<ProfilePicture
displayName={displayVal}
id=""
name={data.profileName || ''}
width={data.avatarWidth || '20'}
/>
</div>

View File

@ -11,13 +11,13 @@
* limitations under the License.
*/
import { AxiosResponse } from 'axios';
import { isEmpty, isEqual } from 'lodash';
import { AxiosError, AxiosResponse } from 'axios';
import { isEmpty, isEqual, isUndefined } from 'lodash';
import AppState from '../AppState';
import { OidcUser } from '../authentication/auth-provider/AuthProvider.interface';
import { getRoles } from '../axiosAPIs/rolesAPI';
import { getTeams } from '../axiosAPIs/teamsAPI';
import { getUsers } from '../axiosAPIs/userAPI';
import { getUserById, getUserByName, getUsers } from '../axiosAPIs/userAPI';
import { API_RES_MAX_SIZE } from '../constants/constants';
import { User } from '../generated/entity/teams/user';
import { getImages } from './CommonUtils';
@ -54,6 +54,7 @@ const getAllRoles = (): void => {
};
export const fetchAllUsers = () => {
AppState.loadUserProfilePics();
getAllUsersList('profile,teams,roles');
getAllTeams();
getAllRoles();
@ -90,3 +91,52 @@ export const matchUserDetails = (
return isMatch;
};
export const fetchUserProfilePic = (userId?: string, username?: string) => {
let promise;
if (userId) {
promise = getUserById(userId, 'profile');
} else if (username) {
promise = getUserByName(username, 'profile');
} else {
return;
}
AppState.updateProfilePicsLoading(userId, username);
promise
.then((res) => {
const userData = res.data as User;
const profile = userData.profile?.images?.image512 || '';
AppState.updateUserProfilePic(userData.id, userData.name, profile);
})
.catch((err: AxiosError) => {
// ignore exception
AppState.updateUserProfilePic(
userId,
username,
err.response?.status === 404 ? '' : undefined
);
})
.finally(() => {
AppState.removeProfilePicsLoading(userId, username);
});
};
export const getUserProfilePic = (userId?: string, username?: string) => {
let profile;
if (userId || username) {
profile = AppState.getUserProfilePic(userId, username);
if (
isUndefined(profile) &&
!AppState.isProfilePicLoading(userId, username)
) {
fetchUserProfilePic(userId, username);
}
}
return profile;
};