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