Unverified Commit 7fdaf8db authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

feat: Update Avatar and Profile Page Loading State (#487)

* Adds shimmering loader to Navigation Avatar

* Adds User PRofile header shimmer loader

* Removing avatar border
parent dc3a4beb
.sb-avatar > div {
border: 1px solid $white;
}
.sb-avatar > img {
margin: 0;
}
......@@ -19,6 +19,9 @@ import SearchBar from 'components/common/SearchBar';
import './styles.scss';
const LOGO_TITLE = 'AMUNDSEN';
const PROFILE_LINK_TEXT = 'My Profile';
// Props
interface StateFromProps {
loggedInUser: LoggedInUser;
......@@ -68,6 +71,14 @@ export class NavBar extends React.Component<NavBarProps> {
};
render() {
const { loggedInUser } = this.props;
const userLink = `/user/${loggedInUser.user_id}?source=navbar`;
let avatar = <div className="shimmering-circle is-shimmer-animated" />;
if (loggedInUser.display_name) {
avatar = <Avatar name={loggedInUser.display_name} size={32} round />;
}
return (
<nav className="container-fluid">
<div className="row">
......@@ -82,51 +93,39 @@ export class NavBar extends React.Component<NavBarProps> {
alt=""
/>
)}
<span className="title-3">AMUNDSEN</span>
<span className="title-3">{LOGO_TITLE}</span>
</Link>
</div>
{this.renderSearchBar()}
<div id="nav-bar-right" className="ml-auto nav-bar-right">
{this.generateNavLinks(AppConfig.navLinks)}
{feedbackEnabled() && <Feedback />}
{this.props.loggedInUser && indexUsersEnabled() && (
{loggedInUser && indexUsersEnabled() && (
<Dropdown id="user-dropdown" pullRight>
<Dropdown.Toggle
noCaret
className="nav-bar-avatar avatar-dropdown"
>
<Avatar
name={this.props.loggedInUser.display_name}
size={32}
round
/>
{avatar}
</Dropdown.Toggle>
<Dropdown.Menu className="profile-menu">
<div className="profile-menu-header">
<div className="title-2">
{this.props.loggedInUser.display_name}
</div>
<div>{this.props.loggedInUser.email}</div>
<div className="title-2">{loggedInUser.display_name}</div>
<div>{loggedInUser.email}</div>
</div>
<MenuItem
componentClass={Link}
id="nav-bar-avatar-link"
to={`/user/${this.props.loggedInUser.user_id}?source=navbar`}
href={`/user/${this.props.loggedInUser.user_id}?source=navbar`}
to={userLink}
href={userLink}
>
My Profile
{PROFILE_LINK_TEXT}
</MenuItem>
</Dropdown.Menu>
</Dropdown>
)}
{this.props.loggedInUser && !indexUsersEnabled() && (
<div className="nav-bar-avatar">
<Avatar
name={this.props.loggedInUser.display_name}
size={32}
round
/>
</div>
{loggedInUser && !indexUsersEnabled() && (
<div className="nav-bar-avatar">{avatar}</div>
)}
</div>
</div>
......
@import 'variables';
$shimmer-loader-circle-size: 32px;
$avatar-container-size: 40px;
.nav-bar {
align-items: center;
background: $nav-bar-color;
......@@ -26,7 +29,7 @@
display: inline-block;
height: 100%;
line-height: $navbar-item-line-height;
padding: 0 8px;
padding: 0 $spacer-1;
&.border-bottom-white:hover,
&.border-bottom-white.active {
......@@ -38,14 +41,18 @@
background-color: $nav-bar-color;
border-radius: 50%;
border-style: none;
height: 40px;
height: $avatar-container-size;
margin-top: -4px;
padding: 0;
width: 40px;
width: $avatar-container-size;
&:not(.avatar-dropdown) {
.sb-avatar {
margin: 4px 0 0 4px;
div {
border: 0 !important;
}
}
}
......@@ -62,6 +69,7 @@
a {
text-decoration: none;
display: inline-block;
}
}
......@@ -71,11 +79,11 @@
.search-bar {
flex-grow: 1;
margin: auto 16px auto auto;
margin: auto $spacer-2 auto auto;
}
.logo-icon {
margin-right: 16px;
margin-right: $spacer-2;
max-height: 32px;
max-width: 144px;
}
......@@ -86,15 +94,21 @@
width: $profile-menu-width;
.profile-menu-header {
padding: 16px 16px 0;
padding: $spacer-2 $spacer-2 0;
}
li {
a {
color: $text-primary;
padding: 16px;
padding: $spacer-2;
width: 100%;
}
}
}
.shimmering-circle {
height: $shimmer-loader-circle-size;
width: $shimmer-loader-circle-size;
border-radius: $shimmer-loader-circle-size;
}
}
......@@ -13,6 +13,8 @@ export const OWNED_TITLE_PREFIX = 'Owned';
export const READ_LABEL = 'frequently used';
export const READ_SOURCE = 'profile_read';
export const READ_TITLE_PREFIX = 'Frequently Used';
export const PROFILE_TEXT = 'Employee Profile';
export const GITHUB_LINK_TEXT = 'Github';
export const EMPTY_TEXT_PREFIX = 'User has no';
export const FOOTER_TEXT_PREFIX = 'View all';
......@@ -17,15 +17,7 @@ import { ResourceType } from 'interfaces/Resources';
import * as LogUtils from 'utils/logUtils';
import { indexDashboardsEnabled } from 'config/config-utils';
import {
AVATAR_SIZE,
BOOKMARKED_LABEL,
BOOKMARKED_SOURCE,
OWNED_LABEL,
OWNED_SOURCE,
READ_LABEL,
READ_SOURCE,
} from './constants';
import { AVATAR_SIZE } from './constants';
import {
mapDispatchToProps,
mapStateToProps,
......@@ -75,19 +67,9 @@ describe('ProfilePage', () => {
return { props, wrapper };
};
describe('constructor', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
});
describe('componentDidMount', () => {
it('calls loadUserInfo', () => {
const { props, wrapper } = setup();
const { wrapper } = setup();
const loadUserInfoSpy = jest.spyOn(wrapper.instance(), 'loadUserInfo');
wrapper.instance().componentDidMount();
expect(loadUserInfoSpy).toHaveBeenCalled();
......@@ -95,14 +77,11 @@ describe('ProfilePage', () => {
});
describe('componentDidUpdate', () => {
let props;
let wrapper;
let loadUserInfoSpy;
beforeEach(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
({ wrapper } = setup());
loadUserInfoSpy = jest.spyOn(wrapper.instance(), 'loadUserInfo');
});
......@@ -121,27 +100,33 @@ describe('ProfilePage', () => {
it('calls getLoggingParams', () => {
const { props, wrapper } = setup();
const getLoggingParamsSpy = jest.spyOn(LogUtils, 'getLoggingParams');
wrapper.instance().loadUserInfo('test');
expect(getLoggingParamsSpy).toHaveBeenCalledWith(props.location.search);
});
it('calls props.getUserById', () => {
const { props, wrapper } = setup();
const { props } = setup();
expect(props.getUserById).toHaveBeenCalled();
});
it('calls props.getUserOwn', () => {
const { props, wrapper } = setup();
const { props } = setup();
expect(props.getUserOwn).toHaveBeenCalled();
});
it('calls props.getUserRead', () => {
const { props, wrapper } = setup();
const { props } = setup();
expect(props.getUserRead).toHaveBeenCalled();
});
it('calls props.getBookmarksForUser', () => {
const { props, wrapper } = setup();
const { props } = setup();
expect(props.getBookmarksForUser).toHaveBeenCalled();
});
});
......@@ -151,10 +136,9 @@ describe('ProfilePage', () => {
let wrapper;
let givenResource;
let content;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
({ props, wrapper } = setup());
givenResource = ResourceType.table;
content = shallow(
<div>{wrapper.instance().generateTabContent(givenResource)}</div>
......@@ -207,6 +191,7 @@ describe('ProfilePage', () => {
it('returns string for tab title according to UI designs', () => {
const { wrapper } = setup();
const givenResource = ResourceType.table;
expect(wrapper.instance().generateTabTitle(givenResource)).toEqual(
'Resource (4)'
);
......@@ -222,9 +207,8 @@ describe('ProfilePage', () => {
let generateTabTitleSpy;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
({ props, wrapper } = setup());
generateTabContentSpy = jest
.spyOn(wrapper.instance(), 'generateTabContent')
.mockImplementation((input) => `${input}Content`);
......@@ -238,6 +222,7 @@ describe('ProfilePage', () => {
describe('pushes tab info for tables', () => {
let tableTab;
beforeAll(() => {
tabInfoArray = wrapper.instance().generateTabInfo();
tableTab = tabInfoArray.find((tab) => tab.key === 'tableKey');
......@@ -261,6 +246,7 @@ describe('ProfilePage', () => {
describe('handle tab info for dashboards', () => {
let dashboardTab;
describe('if dashboards are not enabled', () => {
it('does not render dashboard tab', () => {
mocked(indexDashboardsEnabled).mockImplementationOnce(() => false);
......@@ -305,10 +291,9 @@ describe('ProfilePage', () => {
describe('render', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
({ props, wrapper } = setup());
});
it('renders DocumentTitle w/ correct title', () => {
......
......@@ -37,10 +37,12 @@ import {
BOOKMARKED_TITLE_PREFIX,
EMPTY_TEXT_PREFIX,
FOOTER_TEXT_PREFIX,
GITHUB_LINK_TEXT,
ITEMS_PER_PAGE,
OWNED_LABEL,
OWNED_SOURCE,
OWNED_TITLE_PREFIX,
PROFILE_TEXT,
READ_LABEL,
READ_SOURCE,
READ_TITLE_PREFIX,
......@@ -94,6 +96,7 @@ export class ProfilePage extends React.Component<
componentDidUpdate() {
const { userId } = this.props.match.params;
if (userId !== this.state.userId) {
this.setState({ userId });
this.loadUserInfo(userId);
......@@ -190,87 +193,126 @@ export class ProfilePage extends React.Component<
*/
render() {
const { user } = this.props;
const isLoading = !user.display_name && !user.email && !user.employee_type;
let avatar = null;
if (isLoading) {
avatar = <div className="shimmering-circle is-shimmer-animated" />;
} else if (user.display_name && user.display_name.length > 0) {
avatar = <Avatar name={user.display_name} size={AVATAR_SIZE} round />;
}
let userName = null;
if (isLoading) {
userName = (
<div className="shimmering-text title-text is-shimmer-animated" />
);
} else {
userName = (
<h1 className="h3 header-title-text truncated">
{user.display_name}
{!user.is_active && (
<Flag caseType="sentenceCase" labelStyle="danger" text="Alumni" />
)}
</h1>
);
}
let bullets = null;
if (isLoading) {
bullets = <div className="shimmering-text bullets is-shimmer-animated" />;
} else {
bullets = (
<div className="body-3">
<ul className="header-bullets">
{user.role_name && <li id="user-role">{user.role_name}</li>}
{user.team_name && <li id="team-name">{user.team_name}</li>}
{user.manager_fullname && (
<li id="user-manager">{`Manager: ${user.manager_fullname}`}</li>
)}
</ul>
</div>
);
}
let emailLink = null;
if (isLoading) {
emailLink = (
<div className="shimmering-text header-link is-shimmer-animated" />
);
} else if (user.is_active) {
emailLink = (
<a
id="email-link"
href={`mailto:${user.email}`}
className="btn btn-flat-icon header-link"
target="_blank"
rel="noreferrer"
>
<img className="icon icon-dark icon-mail" />
<span className="email-link-label body-2">{user.email}</span>
</a>
);
}
let profileLink = null;
if (isLoading) {
profileLink = (
<div className="shimmering-text header-link is-shimmer-animated" />
);
} else if (user.is_active && user.profile_url) {
profileLink = (
<a
id="profile-link"
href={user.profile_url}
className="btn btn-flat-icon header-link"
target="_blank"
rel="noreferrer"
>
<span className="icon icon-dark icon-users" />
<span className="profile-link-label body-2">{PROFILE_TEXT}</span>
</a>
);
}
let githubLink = null;
if (isLoading) {
githubLink = (
<div className="shimmering-text header-link is-shimmer-animated" />
);
} else if (user.github_username) {
githubLink = (
<a
id="github-link"
href={`https://github.com/${user.github_username}`}
className="btn btn-flat-icon header-link"
target="_blank"
rel="noreferrer"
>
<img className="icon icon-dark icon-github" />
<span className="github-link-label body-2">{GITHUB_LINK_TEXT}</span>
</a>
);
}
return (
<DocumentTitle title={`${user.display_name} - Amundsen Profile`}>
<main className="resource-detail-layout profile-page">
<header className="resource-header">
<div className="header-section">
<Breadcrumb />
{!isLoading && <Breadcrumb />}
<div id="profile-avatar" className="profile-avatar">
{user.display_name && user.display_name.length > 0 && (
<Avatar name={user.display_name} size={AVATAR_SIZE} round />
)}
{avatar}
</div>
</div>
<div className="header-section header-title">
<h1 className="h3 header-title-text truncated">
{user.display_name}
{!user.is_active && (
<Flag
caseType="sentenceCase"
labelStyle="danger"
text="Alumni"
/>
)}
</h1>
<div className="body-3">
<ul className="header-bullets">
{user.role_name && <li id="user-role">{user.role_name}</li>}
{user.team_name && <li id="team-name">{user.team_name}</li>}
{user.manager_fullname && (
<li id="user-manager">{`Manager: ${user.manager_fullname}`}</li>
)}
</ul>
</div>
{userName}
{bullets}
</div>
<div className="header-section header-links">
{/* { */}
{/* // TODO - Implement deep links to open Slack *!/*/}
{/* user.is_active && user.slack_id &&*/}
{/* <a id="slack-link" href={user.slack_id} className='btn btn-flat-icon header-link' target='_blank'>*/}
{/* <img className='icon icon-dark icon-slack'/>*/}
{/* <span className="body-2">Slack</span>*/}
{/* </a>*/}
{/* } */}
{user.is_active && (
<a
id="email-link"
href={`mailto:${user.email}`}
className="btn btn-flat-icon header-link"
target="_blank"
rel="noreferrer"
>
<img className="icon icon-dark icon-mail" alt="" />
<span className="email-link-label body-2">{user.email}</span>
</a>
)}
{user.is_active && user.profile_url && (
<a
id="profile-link"
href={user.profile_url}
className="btn btn-flat-icon header-link"
target="_blank"
rel="noreferrer"
>
<span className="icon icon-dark icon-users" />
<span className="profile-link-label body-2">
Employee Profile
</span>
</a>
)}
{user.github_username && (
<a
id="github-link"
href={`https://github.com/${user.github_username}`}
className="btn btn-flat-icon header-link"
target="_blank"
rel="noreferrer"
>
<img className="icon icon-dark icon-github" alt="" />
<span className="github-link-label body-2">Github</span>
</a>
)}
{emailLink}
{profileLink}
{githubLink}
</div>
</header>
<div className="profile-body">
......
@import 'variables';
$shimmer-loader-circle-size: 40px;
$shimmering-line-height: 16px;
$shimmering-short-line-width: 150px;
$shimmering-long-line-width: 300px;
$shimmering-link-height: 24px;
$shimmering-link-width: 100px;
.profile-page {
// override height from resource-detail-layout
height: auto;
.profile-avatar {
display: inline-block;
margin: 6px 16px 0 0;
margin: 6px $spacer-2 0 0;
}
.profile-body {
// TODO: consider moving logic for empty content into Tab component
.empty-tab-message {
margin-top: 32px;
margin-top: $spacer-4;
text-align: center;
}
}
......@@ -20,4 +27,29 @@
.resource-list:last-child {
margin-bottom: $spacer-3;
}
.shimmering-circle {
height: $shimmer-loader-circle-size;
width: $shimmer-loader-circle-size;
border-radius: $shimmer-loader-circle-size;
}
.shimmering-text {
height: $shimmering-line-height;
margin: $spacer-1 0;
&.title-text {
width: $shimmering-short-line-width;
}
&.bullets {
width: $shimmering-long-line-width;
}
&.header-link {
width: $shimmering-link-width;
height: $shimmering-link-height;
display: inline-block;
}
}
}
......@@ -7,6 +7,8 @@ import { TableReader } from 'interfaces';
import AppConfig from 'config/config';
import { logClick } from 'ducks/utilMethods';
import './styles.scss';
export interface FrequentUsersProps {
readers: TableReader[];
}
......
@import 'variables';
.frequent-users .sb-avatar > div {
border: 1px solid $btn-default-bg;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment