Unverified Commit 82c36cb6 authored by Daniel's avatar Daniel Committed by GitHub

Update profile page design (#365)

* Modify profile page to new design
* Added breadcrumb back to profile and table pages
* Modified behavior/style of breadcrumb
* Fix styles on the nav-bar dropdown
parent d020ba22
@import 'variables';
$header-height: 84px;
$resource-header-height: 84px;
.resource-detail-layout {
min-width: 1048px;
.resource-header {
display: flex;
height: $header-height;
height: $resource-header-height;
border-bottom: 2px solid $divider;
padding: 16px 24px 0;
......@@ -27,11 +25,33 @@ $header-height: 84px;
max-width: calc(100% - 100px);
}
}
.header-bullets {
display: inline;
margin: 0;
padding: 0;
li {
display: inline;
&:after { content: "\00A0\2022\00A0"; }
&:last-child:after { content: ""; }
}
}
&.header-links {
flex-shrink: 0;
> * {
margin-right: 16px;
}
.header-link {
display: inline-block;
margin: 13px 16px 0 0;
.avatar-label {
font-weight: $font-weight-body-bold;
}
}
}
&.header-buttons {
......@@ -51,7 +71,7 @@ $header-height: 84px;
// 630 | 414+ Flexible layout
.column-layout-1 {
display: flex;
height: calc(100% - #{$header-height});
height: calc(100% - #{$resource-header-height});
> .left-panel {
> .banner {
......
......@@ -85,9 +85,10 @@ $icon-bg-dark: $gray60 !default;
$icon-bg-disabled: $gray20 !default;
// Header & Footer
// Header, Body, & Footer
$nav-bar-color: $indigo100;
$nav-bar-height: 60px;
$body-min-width: 1048px;
$footer-height: 60px;
......
......@@ -15,6 +15,10 @@
// TODO - Move to separate files
// Layout
#main {
min-width: $body-min-width;
}
#main > .container {
margin: 96px auto 48px;
}
......
......@@ -8,7 +8,7 @@ import AppConfig from 'config/config';
import { LinkConfig } from 'config/config-types';
import { GlobalState } from 'ducks/rootReducer';
import { logClick } from 'ducks/utilMethods';
import { Dropdown } from 'react-bootstrap';
import { Dropdown, MenuItem } from 'react-bootstrap';
import { LoggedInUser } from 'interfaces';
......@@ -85,11 +85,13 @@ export class NavBar extends React.Component<NavBarProps> {
<div className='title-2'>{this.props.loggedInUser.display_name}</div>
<div>{this.props.loggedInUser.email}</div>
</div>
<li>
<Link id="nav-bar-avatar-link" to={`/user/${this.props.loggedInUser.user_id}?source=navbar`}>
<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`}>
My Profile
</Link>
</li>
</MenuItem>
</Dropdown.Menu>
</Dropdown>
}
......
......@@ -84,35 +84,11 @@
}
li {
padding: 16px;
a {
color: $text-primary;
padding: 0;
padding: 16px;
width: 100%;
}
}
}
}
.avatar-dropdown {
border-style: none;
padding: 0 !important;
border-radius: 50%;
}
.profile-menu {
$profile-menu-width: 200px;
width: $profile-menu-width;
.profile-menu-header {
padding: 16px 16px 0 16px;
}
li {
padding: 16px;
a {
padding: 0;
}
}
}
......@@ -3,7 +3,7 @@ import * as Avatar from 'react-avatar';
import * as History from 'history';
import { shallow } from 'enzyme';
import { Dropdown } from 'react-bootstrap';
import { Dropdown, MenuItem } from 'react-bootstrap';
import { Link, NavLink } from 'react-router-dom';
import { NavBar, NavBarProps, mapStateToProps } from '../';
......@@ -166,7 +166,7 @@ describe('NavBar', () => {
});
it('renders My Profile link correctly inside of user dropdown', () => {
element = wrapper.find(Dropdown).find(Dropdown.Menu).find(Link).at(0);
element = wrapper.find(Dropdown).find(Dropdown.Menu).find(MenuItem).at(0);
expect(element.children().text()).toEqual('My Profile');
expect(element.props().to).toEqual('/user/test0?source=navbar');
});
......
export const AVATAR_SIZE = 40;
export const BOOKMARKED_LABEL = 'bookmarked';
export const BOOKMARKED_SOURCE = 'profile_bookmark';
export const BOOKMARKED_TAB_KEY = 'bookmark_tab';
......
......@@ -21,6 +21,7 @@ import { GetBookmarksForUserRequest } from 'ducks/bookmark/types';
import { getBookmarksForUser } from 'ducks/bookmark/reducer';
import {
AVATAR_SIZE,
BOOKMARKED_LABEL,
BOOKMARKED_SOURCE,
BOOKMARKED_TAB_KEY,
......@@ -142,78 +143,80 @@ export class ProfilePage extends React.Component<ProfilePageProps, ProfilePageSt
const user = this.props.user;
return (
<DocumentTitle title={ `${user.display_name} - Amundsen Profile` }>
<div className="container profile-page">
<div className="row">
<div className="col-xs-12 col-md-offset-1 col-md-10">
<div className="resource-detail-layout profile-page">
<header className="resource-header">
<div className="header-section">
<Breadcrumb />
{/* TODO - Consider making this part a separate component */}
<div className="profile-header">
<div id="profile-avatar" className="profile-avatar">
{
// default Avatar looks a bit jarring -- intentionally not rendering if no display_name
user.display_name && user.display_name.length > 0 &&
<Avatar name={user.display_name} size={74} round={true} />
}
</div>
<div className="profile-details">
<div id="profile-title" className="profile-title">
<h1>{ user.display_name }</h1>
{
(!user.is_active) &&
<Flag caseType="sentenceCase" labelStyle="danger" text="Alumni"/>
}
</div>
{
user.role_name && user.team_name &&
<div id="user-role" className="body-2">{ `${user.role_name} on ${user.team_name}` }</div>
}
{/*TODO - delete when 'role_name'/'title' is added to user object in backend */}
{
!user.role_name && user.team_name &&
<div id="user-role" className="body-2">{ `Team: ${user.team_name}` }</div>
}
{
user.manager_fullname &&
<div id="user-manager" className="body-2">{ `Manager: ${user.manager_fullname}` }</div>
}
<div className="profile-icons">
{/*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' 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' target='_blank'>
<img className='icon icon-dark icon-mail'/>
<span className="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' target='_blank'>
<img className='icon icon-dark icon-users'/>
<span className="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' target='_blank'>
<img className='icon icon-dark icon-github'/>
<span className="body-2">Github</span>
</a>
}
</div>
</div>
<div id="profile-avatar" className="profile-avatar">
{
user.display_name && user.display_name.length > 0 &&
<Avatar name={user.display_name} size={AVATAR_SIZE} round={true} />
}
</div>
<div id="profile-tabs" className="profile-tabs">
<Tabs tabs={ this.generateTabInfo() } defaultTab={ BOOKMARKED_TAB_KEY } />
</div>
<div className="header-section header-title">
<h3 className="header-title-text truncated">
{ user.display_name }
{
(!user.is_active) &&
<Flag caseType="sentenceCase" labelStyle="danger" text="Alumni"/>
}
</h3>
<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>
</div>
</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'>
<img className='icon icon-dark icon-mail'/>
<span className="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'>
<img className='icon icon-dark icon-users'/>
<span className="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'>
<img className='icon icon-dark icon-github'/>
<span className="body-2">Github</span>
</a>
}
</div>
</header>
<main>
<div className="profile-tabs">
<Tabs tabs={ this.generateTabInfo() } defaultTab={ BOOKMARKED_TAB_KEY } />
</div>
</main>
</div>
</DocumentTitle>
);
......
@import 'variables';
.profile-page {
.profile-avatar {
display: inline-block;
margin: 6px 16px 0 0;
}
.profile-header {
display: flex;
.profile-avatar {
.header-title-text {
.flag {
margin-left: 8px;
margin-right: 30px;
}
}
.profile-details {
display: flex;
flex-direction: column;
h1 {
margin-bottom: 0;
margin-top: 0;
}
.profile-icons {
margin-top: 12px;
a {
margin-left: 8px;
margin-right: 8px;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
.profile-title {
display: flex;
margin-bottom: 8px;
.profile-tabs {
margin-top: 24px;
.flag {
height: min-content; // TODO: consider moving height into Flag component
font-size: 16px;
margin: auto auto 4px 12px;
}
}
.nav-tabs {
margin-left: 16px;
}
.list-group {
margin-top: 0;
}
}
.profile-tabs {
margin-top: 64px;
// TODO: consider moving logic for empty content into Tab component
.empty-tab-message {
......
......@@ -14,6 +14,7 @@ import { getMockRouterProps } from 'fixtures/mockRouter';
import { ResourceType } from 'interfaces/Resources';
import {
AVATAR_SIZE,
BOOKMARKED_LABEL,
BOOKMARKED_SOURCE,
BOOKMARKED_TAB_KEY,
......@@ -235,7 +236,7 @@ describe('ProfilePage', () => {
it('renders Avatar for user.display_name', () => {
expect(wrapper.find(Avatar).props()).toMatchObject({
name: props.user.display_name,
size: 74,
size: AVATAR_SIZE,
round: true,
});
});
......@@ -253,7 +254,7 @@ describe('ProfilePage', () => {
});
it('renders header with display_name', () => {
expect(wrapper.find('#profile-title').find('h1').text()).toEqual(props.user.display_name);
expect(wrapper.find('.header-title-text').text()).toContain(props.user.display_name);
});
it('renders Flag with correct props if user not active', () => {
......@@ -264,7 +265,7 @@ describe('ProfilePage', () => {
const wrapper = setup({
user: userCopy,
}).wrapper;
expect(wrapper.find('#profile-title').find(Flag).props()).toMatchObject({
expect(wrapper.find('.header-title-text').find(Flag).props()).toMatchObject({
caseType: 'sentenceCase',
labelStyle: 'danger',
text: 'Alumni',
......@@ -272,11 +273,11 @@ describe('ProfilePage', () => {
});
it('renders user role', () => {
expect(wrapper.find('#user-role').text()).toEqual('Tester on QA');
expect(wrapper.find('#user-role').text()).toEqual('Tester');
});
it('renders user manager', () => {
expect(wrapper.find('#user-manager').text()).toEqual('Manager: Test Manager');
it('renders user team name', () => {
expect(wrapper.find('#team-name').text()).toEqual('QA');
});
it('renders user manager', () => {
......@@ -292,14 +293,14 @@ describe('ProfilePage', () => {
});
it('renders Tabs w/ correct props', () => {
expect(wrapper.find('#profile-tabs').find(Tabs).props()).toMatchObject({
expect(wrapper.find('.profile-tabs').find(Tabs).props()).toMatchObject({
tabs: wrapper.instance().generateTabInfo(),
defaultTab: BOOKMARKED_TAB_KEY,
});
});
describe('if user.is_active', () => {
// TODO - Uncomment when slack integration is fixed
// TODO - Uncomment when slack integration is built
// it('renders slack link with correct href', () => {
// expect(wrapper.find('#slack-link').props().href).toEqual('www.slack.com');
// });
......
......@@ -105,13 +105,13 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
} else {
const data = this.props.tableData;
innerContent = (
<div className="resource-detail-layout table-detail-2">
<div className="resource-detail-layout table-detail">
{
notificationsEnabled() && <RequestMetadataForm />
}
<header className="resource-header">
<div className="header-section">
{/* TODO - add Breadcrumb here */}
<Breadcrumb />
<img className={"icon icon-header " + getDatabaseIconClass(data.database)} />
</div>
<div className="header-section header-title">
......@@ -120,11 +120,11 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
</h3>
<BookmarkIcon bookmarkKey={ this.props.tableData.key }/>
<div className="body-2">
Datasets &bull;&nbsp;
{ getDatabaseDisplayName(data.database) }
&nbsp;&bull;&nbsp;
{ data.cluster }
&nbsp;
<ul className="header-bullets">
<li>Datasets</li>
<li>{ getDatabaseDisplayName(data.database) }</li>
<li>{ data.cluster }</li>
</ul>
{
data.badges.length > 0 &&
<BadgeList badges={ data.badges } />
......
@import 'variables';
.table-detail-2 {
.table-detail {
height: calc(100% - #{$nav-bar-height} - #{$footer-height});
.header-link {
display: inline-block;
margin: 13px 0;
.avatar-label {
font-weight: $font-weight-body-bold;
}
}
.column-layout-1 .left-panel {
.section-title {
color: $text-tertiary;
......
......@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import './styles.scss';
import { GlobalState } from 'ducks/rootReducer';
import { loadPreviousSearch } from 'ducks/search/reducer';
import { LoadPreviousSearchRequest } from 'ducks/search/types';
......@@ -13,51 +12,35 @@ export interface OwnProps {
text?: string;
}
export interface StateFromProps {
searchTerm: string;
}
export interface MapDispatchToProps {
loadPreviousSearch: () => LoadPreviousSearchRequest;
}
export type BreadcrumbProps = OwnProps & StateFromProps & MapDispatchToProps;
export type BreadcrumbProps = OwnProps & MapDispatchToProps;
export const Breadcrumb: React.SFC<BreadcrumbProps> = (props) => {
let path = props.path;
let text = props.text;
if (!path && !text) {
path = '/';
text = 'Home';
if (props.searchTerm) {
return (
<div className="amundsen-breadcrumb">
<a onClick={ props.loadPreviousSearch } className='btn btn-flat-icon title-3'>
<img className='icon icon-left'/>
<span>Search Results</span>
</a>
</div>
);
}
const { path, text } = props;
if (path !== undefined && text !== undefined) {
return (
<div className="amundsen-breadcrumb">
<Link to={path} className='btn btn-flat-icon title-3'>
<img className='icon icon-left'/>
<span>{text}</span>
</Link>
</div>
);
}
return (
<div className="amundsen-breadcrumb">
<Link to={path} className='btn btn-flat-icon title-3'>
<a onClick={ props.loadPreviousSearch } className='btn btn-flat-icon title-3'>
<img className='icon icon-left'/>
<span>{text}</span>
</Link>
</a>
</div>
);
};
export const mapStateToProps = (state: GlobalState) => {
return {
searchTerm: state.search.search_term,
};
};
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ loadPreviousSearch }, dispatch);
};
export default connect<StateFromProps, MapDispatchToProps>(mapStateToProps, mapDispatchToProps)(Breadcrumb);
export default connect<{}, MapDispatchToProps>(null, mapDispatchToProps)(Breadcrumb);
@import 'variables';
// Margins values chosen for the breadcrumb to sort-of split the difference/center
// itself in our 96px/64px/32px top margins.
.amundsen-breadcrumb {
height: 24px;
margin-top: -72px;
margin-bottom: 48px;
display: inline-block;
img.icon-left {
margin: -3px 0 -3px -8px;
......@@ -17,17 +14,3 @@
line-height: 24px;
}
}
@media (max-width: $screen-md-max) {
.amundsen-breadcrumb {
margin-top: -40px;
margin-bottom: 16px;
}
}
@media (max-width: $screen-sm-max) {
.amundsen-breadcrumb {
margin-top: -10px;
margin-bottom: 10px;
}
}
......@@ -3,8 +3,7 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import { Link } from 'react-router-dom';
import { Breadcrumb, BreadcrumbProps, mapStateToProps, mapDispatchToProps } from '../';
import globalState from '../../../../fixtures/globalState';
import { Breadcrumb, BreadcrumbProps, mapDispatchToProps } from '../';
describe('Breadcrumb', () => {
let props: BreadcrumbProps;
......@@ -15,7 +14,6 @@ describe('Breadcrumb', () => {
props = {
path: 'testPath',
text: 'testText',
searchTerm: '',
loadPreviousSearch: jest.fn(),
};
subject = shallow(<Breadcrumb {...props} />);
......@@ -35,7 +33,6 @@ describe('Breadcrumb', () => {
describe('render with existing searchTerm', () => {
beforeEach(() => {
props = {
searchTerm: 'testTerm',
loadPreviousSearch: jest.fn(),
};
subject = shallow(<Breadcrumb {...props} />);
......@@ -46,10 +43,6 @@ describe('Breadcrumb', () => {
onClick: props.loadPreviousSearch,
});
});
it('renders Link with correct text', () => {
expect(subject.find('a').find('span').text()).toEqual('Search Results');
});
});
describe('render with existing searchTerm and prop overrides', () => {
......@@ -57,7 +50,6 @@ describe('Breadcrumb', () => {
props = {
path: 'testPath',
text: 'testText',
searchTerm: 'testTerm',
loadPreviousSearch: jest.fn(),
};
subject = shallow(<Breadcrumb {...props} />);
......@@ -74,17 +66,6 @@ describe('Breadcrumb', () => {
});
});
describe('mapStateToProps', () => {
let result;
beforeAll(() => {
result = mapStateToProps(globalState);
});
it('sets searchTerm on the props', () => {
expect(result.searchTerm).toEqual(globalState.search.search_term);
});
});
describe('mapDispatchToProps', () => {
let dispatch;
let result;
......
......@@ -40,7 +40,7 @@ import {
setPageIndex, setResource,
} from './reducer';
import { autoSelectResource, getPageIndex, getSearchState } from './utils';
import { updateSearchUrl } from 'utils/navigation-utils';
import { BrowserHistory, updateSearchUrl } from 'utils/navigation-utils';
export function* inlineSearchWorker(action: InlineSearchRequest): SagaIterator {
const { term } = action.payload;
......@@ -204,6 +204,10 @@ export function* urlDidUpdateWatcher(): SagaIterator {
export function* loadPreviousSearchWorker(action: LoadPreviousSearchRequest): SagaIterator {
const state = yield select(getSearchState);
if (state.search_term === "") {
BrowserHistory.goBack();
return;
}
updateSearchUrl({
term: state.search_term,
resource: state.selectedTab,
......
......@@ -589,6 +589,8 @@ describe('search ducks', () => {
});
describe('loadPreviousSearchWorker', () => {
// TODO - test 'BrowserHistory.goBack' case
it('applies the existing search state into the URL', () => {
updateSearchUrlSpy.mockClear();
......
......@@ -9,8 +9,6 @@ import { createStore, applyMiddleware } from 'redux';
import { Router, Route, Switch } from 'react-router-dom';
import DocumentTitle from 'react-document-title';
import { feedbackEnabled } from 'config/config-utils';
import AnnouncementPage from './components/AnnouncementPage';
import BrowsePage from './components/BrowsePage';
import Footer from './components/Footer';
......
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