Unverified Commit 2612b4b9 authored by Daniel's avatar Daniel Committed by GitHub

Integrate metadata user API (#90)

* Integrate metadata '/user' API
* Update UserSchema/User class and TS interfaces
* '/auth_user' API now piggybacks on the metadata '/user' API
* Move user test data into fixtures/globalState.ts
parent 41735ddd
...@@ -13,9 +13,10 @@ from amundsen_application.models.user import load_user, dump_user ...@@ -13,9 +13,10 @@ from amundsen_application.models.user import load_user, dump_user
from amundsen_application.api.utils.request_utils import get_query_param, request_wrapper from amundsen_application.api.utils.request_utils import get_query_param, request_wrapper
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
REQUEST_SESSION_TIMEOUT = 10 REQUEST_SESSION_TIMEOUT_SEC = 3
metadata_blueprint = Blueprint('metadata', __name__, url_prefix='/api/metadata/v0') metadata_blueprint = Blueprint('metadata', __name__, url_prefix='/api/metadata/v0')
...@@ -23,6 +24,7 @@ TABLE_ENDPOINT = '/table' ...@@ -23,6 +24,7 @@ TABLE_ENDPOINT = '/table'
LAST_INDEXED_ENDPOINT = '/latest_updated_ts' LAST_INDEXED_ENDPOINT = '/latest_updated_ts'
POPULAR_TABLES_ENDPOINT = '/popular_tables/' POPULAR_TABLES_ENDPOINT = '/popular_tables/'
TAGS_ENDPOINT = '/tags/' TAGS_ENDPOINT = '/tags/'
USER_ENDPOINT = '/user'
def _get_table_endpoint() -> str: def _get_table_endpoint() -> str:
...@@ -72,7 +74,7 @@ def popular_tables() -> Response: ...@@ -72,7 +74,7 @@ def popular_tables() -> Response:
url=url, url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'], client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'], headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code status_code = response.status_code
...@@ -124,7 +126,7 @@ def _send_metadata_get_request(url: str) -> Response: ...@@ -124,7 +126,7 @@ def _send_metadata_get_request(url: str) -> Response:
url=url, url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'], client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'], headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
def _get_partition_data(watermarks: Dict) -> Dict: def _get_partition_data(watermarks: Dict) -> Dict:
...@@ -238,7 +240,7 @@ def _update_table_owner(*, table_key: str, method: str, owner: str) -> Dict[str, ...@@ -238,7 +240,7 @@ def _update_table_owner(*, table_key: str, method: str, owner: str) -> Dict[str,
url=url, url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'], client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'], headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
# TODO: Figure out a way to get this payload from flask.jsonify which wraps with app's response_class # TODO: Figure out a way to get this payload from flask.jsonify which wraps with app's response_class
return {'msg': 'Updated owner'} return {'msg': 'Updated owner'}
...@@ -275,7 +277,7 @@ def get_last_indexed() -> Response: ...@@ -275,7 +277,7 @@ def get_last_indexed() -> Response:
url=url, url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'], client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'], headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code status_code = response.status_code
...@@ -305,7 +307,7 @@ def get_table_description() -> Response: ...@@ -305,7 +307,7 @@ def get_table_description() -> Response:
url=url, url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'], client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'], headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code status_code = response.status_code
...@@ -337,7 +339,7 @@ def get_column_description() -> Response: ...@@ -337,7 +339,7 @@ def get_column_description() -> Response:
url=url, url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'], client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'], headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code status_code = response.status_code
...@@ -379,7 +381,7 @@ def put_table_description() -> Response: ...@@ -379,7 +381,7 @@ def put_table_description() -> Response:
url=url, url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'], client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'], headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code status_code = response.status_code
...@@ -421,7 +423,7 @@ def put_column_description() -> Response: ...@@ -421,7 +423,7 @@ def put_column_description() -> Response:
url=url, url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'], client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'], headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code status_code = response.status_code
...@@ -452,7 +454,7 @@ def get_tags() -> Response: ...@@ -452,7 +454,7 @@ def get_tags() -> Response:
url=url, url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'], client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'], headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code status_code = response.status_code
...@@ -497,7 +499,7 @@ def update_table_tags() -> Response: ...@@ -497,7 +499,7 @@ def update_table_tags() -> Response:
url=url, url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'], client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'], headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code status_code = response.status_code
...@@ -516,7 +518,40 @@ def update_table_tags() -> Response: ...@@ -516,7 +518,40 @@ def update_table_tags() -> Response:
return make_response(payload, HTTPStatus.INTERNAL_SERVER_ERROR) return make_response(payload, HTTPStatus.INTERNAL_SERVER_ERROR)
# TODO: Implement
@metadata_blueprint.route('/user', methods=['GET']) @metadata_blueprint.route('/user', methods=['GET'])
def get_user() -> Response: def get_user() -> Response:
return make_response(jsonify({'msg': 'Not implemented'}), HTTPStatus.NOT_IMPLEMENTED)
@action_logging
def _log_get_user(*, user_id: str) -> None:
pass # pragma: no cover
try:
user_id = get_query_param(request.args, 'user_id')
url = '{0}{1}/{2}'.format(app.config['METADATASERVICE_BASE'], USER_ENDPOINT, user_id)
_log_get_user(user_id=user_id)
response = request_wrapper(method='GET',
url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code
if status_code == HTTPStatus.OK:
message = 'Success'
else:
message = 'Encountered error: failed to fetch user with user_id: {0}'.format(user_id)
logging.error(message)
payload = {
'msg': message,
'user': dump_user(load_user(response.json())),
}
return make_response(jsonify(payload), status_code)
except Exception as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
payload = jsonify({'msg': message})
return make_response(payload, HTTPStatus.INTERNAL_SERVER_ERROR)
...@@ -13,7 +13,7 @@ from amundsen_application.api.utils.request_utils import get_query_param, reques ...@@ -13,7 +13,7 @@ from amundsen_application.api.utils.request_utils import get_query_param, reques
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
REQUEST_SESSION_TIMEOUT = 10 REQUEST_SESSION_TIMEOUT_SEC = 3
search_blueprint = Blueprint('search', __name__, url_prefix='/api/search/v0') search_blueprint = Blueprint('search', __name__, url_prefix='/api/search/v0')
...@@ -201,7 +201,7 @@ def _search_table(*, search_term: str, page_index: int) -> Dict[str, Any]: ...@@ -201,7 +201,7 @@ def _search_table(*, search_term: str, page_index: int) -> Dict[str, Any]:
url=url, url=url,
client=app.config['SEARCHSERVICE_REQUEST_CLIENT'], client=app.config['SEARCHSERVICE_REQUEST_CLIENT'],
headers=app.config['SEARCHSERVICE_REQUEST_HEADERS'], headers=app.config['SEARCHSERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT) timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code status_code = response.status_code
......
import logging import logging
from flask import Response from http import HTTPStatus
from flask import Response, jsonify, make_response
from flask import current_app as app from flask import current_app as app
from flask.blueprints import Blueprint from flask.blueprints import Blueprint
from amundsen_application.models.user import load_user from amundsen_application.api.metadata.v0 import USER_ENDPOINT
from amundsen_application.api.utils.request_utils import request_wrapper
from amundsen_application.models.user import load_user, dump_user
REQUEST_SESSION_TIMEOUT_SEC = 3
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
...@@ -13,9 +19,34 @@ blueprint = Blueprint('api', __name__, url_prefix='/api') ...@@ -13,9 +19,34 @@ blueprint = Blueprint('api', __name__, url_prefix='/api')
@blueprint.route('/auth_user', methods=['GET']) @blueprint.route('/auth_user', methods=['GET'])
def current_user() -> Response: def current_user() -> Response:
if (app.config['AUTH_USER_METHOD']): try:
if app.config['AUTH_USER_METHOD']:
user = app.config['AUTH_USER_METHOD'](app) user = app.config['AUTH_USER_METHOD'](app)
else: else:
user = load_user({'user_id': 'undefined', 'display_name': '*'}) raise Exception('AUTH_USER_METHOD is not configured')
url = '{0}{1}/{2}'.format(app.config['METADATASERVICE_BASE'], USER_ENDPOINT, user.user_id)
response = request_wrapper(method='GET',
url=url,
client=app.config['METADATASERVICE_REQUEST_CLIENT'],
headers=app.config['METADATASERVICE_REQUEST_HEADERS'],
timeout_sec=REQUEST_SESSION_TIMEOUT_SEC)
status_code = response.status_code
if status_code == HTTPStatus.OK:
message = 'Success'
else:
message = 'Encountered error: failed to fetch user with user_id: {0}'.format(user.user_id)
logging.error(message)
return user.to_json() payload = {
'msg': message,
'user': dump_user(load_user(response.json()))
}
return make_response(jsonify(payload), status_code)
except Exception as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
payload = jsonify({'msg': message})
return make_response(payload, HTTPStatus.INTERNAL_SERVER_ERROR)
...@@ -13,35 +13,36 @@ redesign how User handles names ...@@ -13,35 +13,36 @@ redesign how User handles names
class User: class User:
# TODO: alphabetize after we have the real params
def __init__(self, def __init__(self,
display_name: str = None,
email: str = None,
employee_type: str = None,
first_name: str = None, first_name: str = None,
full_name: str = None,
github_username: str = None,
is_active: bool = True,
last_name: str = None, last_name: str = None,
email: str = None, manager_fullname: str = None,
display_name: str = None,
profile_url: str = None, profile_url: str = None,
user_id: str = None,
github_name: str = None,
is_active: bool = True,
manager_name: str = None,
role_name: str = None, role_name: str = None,
slack_url: str = None, slack_id: str = None,
team_name: str = None) -> None: team_name: str = None,
user_id: str = None) -> None:
self.display_name = display_name
self.email = email
self.employee_type = employee_type
self.first_name = first_name self.first_name = first_name
self.full_name = full_name
self.github_username = github_username
self.is_active = is_active
self.last_name = last_name self.last_name = last_name
self.email = email self.manager_fullname = manager_fullname
self.display_name = display_name
self.profile_url = profile_url self.profile_url = profile_url
# TODO: modify the following names as needed after backend support is implemented
self.user_id = user_id
self.github_name = github_name
self.is_active = is_active
self.manager_name = manager_name
self.role_name = role_name self.role_name = role_name
self.slack_url = slack_url self.slack_id = slack_id
self.team_name = team_name self.team_name = team_name
# TODO: frequent_used, bookmarked, & owned resources self.user_id = user_id
# TODO: Add frequent_used, bookmarked, & owned resources
def to_json(self) -> Response: def to_json(self) -> Response:
user_info = dump_user(self) user_info = dump_user(self)
...@@ -49,40 +50,34 @@ class User: ...@@ -49,40 +50,34 @@ class User:
class UserSchema(Schema): class UserSchema(Schema):
display_name = fields.Str(allow_none=True)
email = fields.Str(allow_none=True)
employee_type = fields.Str(allow_none=True)
first_name = fields.Str(allow_none=True) first_name = fields.Str(allow_none=True)
full_name = fields.Str(allow_none=True)
github_username = fields.Str(allow_none=True)
is_active = fields.Bool(allow_none=True)
last_name = fields.Str(allow_none=True) last_name = fields.Str(allow_none=True)
email = fields.Str(allow_none=True) manager_fullname = fields.Str(allow_none=True)
display_name = fields.Str(required=True)
profile_url = fields.Str(allow_none=True) profile_url = fields.Str(allow_none=True)
user_id = fields.Str(required=True)
github_name = fields.Str(allow_none=True)
is_active = fields.Bool(allow_none=True)
manager_name = fields.Str(allow_none=True)
role_name = fields.Str(allow_none=True) role_name = fields.Str(allow_none=True)
slack_url = fields.Str(allow_none=True) slack_id = fields.Str(allow_none=True)
team_name = fields.Str(allow_none=True) team_name = fields.Str(allow_none=True)
user_id = fields.Str(required=True)
@pre_load @pre_load
def generate_display_name(self, data: Dict) -> Dict: def preprocess_data(self, data: Dict) -> Dict:
if data.get('display_name', None): if not data.get('user_id', None):
return data data['user_id'] = data.get('email', None)
if data.get('first_name', None) or data.get('last_name', None):
data['display_name'] = "{} {}".format(data.get('first_name', ''), data.get('last_name', '')).strip()
return data
data['display_name'] = data.get('email', None)
return data
@pre_load
def generate_profile_url(self, data: Dict) -> Dict:
if data.get('profile_url', None):
return data
if not data.get('profile_url', None):
data['profile_url'] = '' data['profile_url'] = ''
if app.config['GET_PROFILE_URL']: if app.config['GET_PROFILE_URL']:
data['profile_url'] = app.config['GET_PROFILE_URL'](data['display_name']) data['profile_url'] = app.config['GET_PROFILE_URL'](data['user_id'])
if not data.get('display_name', None):
data['display_name'] = data.get('full_name', data.get('email'))
return data return data
@post_load @post_load
...@@ -92,7 +87,8 @@ class UserSchema(Schema): ...@@ -92,7 +87,8 @@ class UserSchema(Schema):
@validates_schema @validates_schema
def validate_user(self, data: Dict) -> None: def validate_user(self, data: Dict) -> None:
if not data.get('display_name', None): if not data.get('display_name', None):
raise ValidationError('One or more must be provided: "first_name", "last_name", "email", "display_name"') raise ValidationError('"display_name" must be provided')
if not data.get('user_id', None): if not data.get('user_id', None):
raise ValidationError('"user_id" must be provided') raise ValidationError('"user_id" must be provided')
......
...@@ -37,10 +37,7 @@ import globalState from 'fixtures/globalState'; ...@@ -37,10 +37,7 @@ import globalState from 'fixtures/globalState';
describe('NavBar', () => { describe('NavBar', () => {
const setup = (propOverrides?: Partial<NavBarProps>) => { const setup = (propOverrides?: Partial<NavBarProps>) => {
const props: NavBarProps = { const props: NavBarProps = {
loggedInUser: { loggedInUser: globalState.user.loggedInUser,
user_id: 'test0',
display_name: 'Test User',
},
getLoggedInUser: jest.fn(), getLoggedInUser: jest.fn(),
...propOverrides ...propOverrides
}; };
...@@ -108,7 +105,7 @@ describe('NavBar', () => { ...@@ -108,7 +105,7 @@ describe('NavBar', () => {
it('renders homepage Link with correct text', () => { it('renders homepage Link with correct text', () => {
element = wrapper.find('#nav-bar-left').find(Link); element = wrapper.find('#nav-bar-left').find(Link);
expect(element.children().text()).toEqual('AMUNDSEN'); expect(element.children().text()).toEqual('AMUNDSEN');
}) });
it('calls generateNavLinks with correct props', () => { it('calls generateNavLinks with correct props', () => {
expect(spy).toHaveBeenCalledWith(AppConfig.navLinks); expect(spy).toHaveBeenCalledWith(AppConfig.navLinks);
......
...@@ -47,10 +47,10 @@ export class ProfilePage extends React.Component<ProfilePageProps> { ...@@ -47,10 +47,10 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
createEmptyTabMessage = (message: string) => { createEmptyTabMessage = (message: string) => {
return ( return (
<div className="empty-tab-message"> <div className="empty-tab-message">
<text>{ message }</text> <label>{ message }</label>
</div> </div>
); );
} };
generateTabInfo = () => { generateTabInfo = () => {
const user = this.props.user; const user = this.props.user;
...@@ -75,7 +75,7 @@ export class ProfilePage extends React.Component<ProfilePageProps> { ...@@ -75,7 +75,7 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
}); });
return tabInfo; return tabInfo;
} };
/* TODO: Add support to direct to 404 page for edgecase of someone typing in /* TODO: Add support to direct to 404 page for edgecase of someone typing in
or pasting in a bad url. This would be consistent with TableDetail page behavior */ or pasting in a bad url. This would be consistent with TableDetail page behavior */
...@@ -84,6 +84,8 @@ export class ProfilePage extends React.Component<ProfilePageProps> { ...@@ -84,6 +84,8 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
return ( return (
<DocumentTitle title={ `${user.display_name} - Amundsen Profile` }> <DocumentTitle title={ `${user.display_name} - Amundsen Profile` }>
<div className="container profile-page"> <div className="container profile-page">
<div className="row">
<div className="col-xs-12 col-md-offset-1 col-md-10">
<Breadcrumb path='/' text='Search Results'/> <Breadcrumb path='/' text='Search Results'/>
<div className="profile-header"> <div className="profile-header">
<div id="profile-avatar" className="profile-avatar"> <div id="profile-avatar" className="profile-avatar">
...@@ -101,12 +103,18 @@ export class ProfilePage extends React.Component<ProfilePageProps> { ...@@ -101,12 +103,18 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
<Flag caseType="sentenceCase" labelStyle="label-danger" text="Alumni"/> <Flag caseType="sentenceCase" labelStyle="label-danger" text="Alumni"/>
} }
</div> </div>
<text id="user-role">{ `${user.role_name} on ${user.team_name}` }</text> {
<text id="user-manager">{ `Manager: ${user.manager_name}` }</text> user.role_name && user.team_name &&
<label id="user-role">{ `${user.role_name} on ${user.team_name}` }</label>
}
{
user.manager_fullname &&
<label id="user-manager">{ `Manager: ${user.manager_fullname}` }</label>
}
<div className="profile-icons"> <div className="profile-icons">
{ {
user.is_active && user.is_active &&
<a id="slack-link" href={user.slack_url} className='btn btn-flat-icon' target='_blank'> <a id="slack-link" href={user.slack_id} className='btn btn-flat-icon' target='_blank'>
<img className='icon icon-slack'/> <img className='icon icon-slack'/>
<span>Slack</span> <span>Slack</span>
</a> </a>
...@@ -119,16 +127,19 @@ export class ProfilePage extends React.Component<ProfilePageProps> { ...@@ -119,16 +127,19 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
</a> </a>
} }
{ {
user.is_active && user.is_active && user.profile_url &&
<a id="profile-link" href={user.profile_url} className='btn btn-flat-icon' target='_blank'> <a id="profile-link" href={user.profile_url} className='btn btn-flat-icon' target='_blank'>
<img className='icon icon-users'/> <img className='icon icon-users'/>
<span>Employee Profile</span> <span>Employee Profile</span>
</a> </a>
} }
<a id="github-link" href={`https://github.com/${user.github_name}`} className='btn btn-flat-icon' target='_blank'> {
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-github'/> <img className='icon icon-github'/>
<span>Github</span> <span>Github</span>
</a> </a>
}
</div> </div>
</div> </div>
</div> </div>
...@@ -136,6 +147,8 @@ export class ProfilePage extends React.Component<ProfilePageProps> { ...@@ -136,6 +147,8 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
<Tabs tabs={ this.generateTabInfo() } defaultTab='frequentUses_tab' /> <Tabs tabs={ this.generateTabInfo() } defaultTab='frequentUses_tab' />
</div> </div>
</div> </div>
</div>
</div>
</DocumentTitle> </DocumentTitle>
); );
} }
...@@ -145,7 +158,7 @@ export const mapStateToProps = (state: GlobalState) => { ...@@ -145,7 +158,7 @@ export const mapStateToProps = (state: GlobalState) => {
return { return {
user: state.user.profileUser, user: state.user.profileUser,
} }
} };
export const mapDispatchToProps = (dispatch) => { export const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ getUserById }, dispatch); return bindActionCreators({ getUserById }, dispatch);
......
@import 'variables'; @import 'variables';
.profile-page { .profile-page {
margin-bottom: 48px;
}
.profile-header { .profile-header {
display: flex; display: flex;
.profile-avatar { .profile-avatar {
margin-left: 24px; margin-left: 8px;
margin-right: 24px; margin-right: 30px;
} }
.profile-details { .profile-details {
...@@ -17,8 +15,8 @@ ...@@ -17,8 +15,8 @@
flex-direction: column; flex-direction: column;
h1 { h1 {
margin-bottom: 0px; margin-bottom: 0;
margin-top: 0px; margin-top: 0;
} }
.profile-icons { .profile-icons {
...@@ -29,10 +27,10 @@ ...@@ -29,10 +27,10 @@
margin-right: 8px; margin-right: 8px;
&:first-child { &:first-child {
margin-left: 0px; margin-left: 0;
} }
&:last-child { &:last-child {
margin-right: 0px; margin-right: 0;
} }
} }
} }
...@@ -49,15 +47,16 @@ ...@@ -49,15 +47,16 @@
} }
} }
} }
.profile-tabs { .profile-tabs {
margin-top: 48px; margin-top: 64px;
}
// 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 {
color: $gray-light; color: $gray-light;
margin-top: 64px; margin-top: 64px;
text-align: center; text-align: center;
}
}
} }
...@@ -14,18 +14,7 @@ import globalState from 'fixtures/globalState'; ...@@ -14,18 +14,7 @@ import globalState from 'fixtures/globalState';
describe('ProfilePage', () => { describe('ProfilePage', () => {
const setup = (propOverrides?: Partial<ProfilePageProps>) => { const setup = (propOverrides?: Partial<ProfilePageProps>) => {
const props: ProfilePageProps = { const props: ProfilePageProps = {
user: { user: globalState.user.profileUser,
user_id: 'test0',
display_name: 'Test User',
email: 'test@test.com',
github_name: 'githubName',
is_active: true,
manager_name: 'Test Manager',
profile_url: 'www.test.com',
role_name: 'Tester',
slack_url: 'www.slack.com',
team_name: 'QA',
},
getUserById: jest.fn(), getUserById: jest.fn(),
...propOverrides ...propOverrides
}; };
...@@ -73,7 +62,7 @@ describe('ProfilePage', () => { ...@@ -73,7 +62,7 @@ describe('ProfilePage', () => {
}); });
it('creates text with given message', () => { it('creates text with given message', () => {
expect(shallow(content).find('text').text()).toEqual('Empty message'); expect(shallow(content).find('label').text()).toEqual('Empty message');
}); });
}); });
...@@ -119,19 +108,13 @@ describe('ProfilePage', () => { ...@@ -119,19 +108,13 @@ describe('ProfilePage', () => {
}); });
it('does not render Avatar if user.display_name is empty string', () => { it('does not render Avatar if user.display_name is empty string', () => {
const userCopy = {
...globalState.user.profileUser,
display_name: "",
} ;
const wrapper = setup({ const wrapper = setup({
user: { user: userCopy,
user_id: 'test0',
display_name: '',
email: 'test@test.com',
github_name: 'githubName',
is_active: true,
manager_name: 'Test Manager',
profile_url: 'www.test.com',
role_name: 'Tester',
slack_url: 'www.slack.com',
team_name: 'QA',
}
}).wrapper; }).wrapper;
expect(wrapper.find('#profile-avatar').children().exists()).toBeFalsy(); expect(wrapper.find('#profile-avatar').children().exists()).toBeFalsy();
}); });
...@@ -141,19 +124,12 @@ describe('ProfilePage', () => { ...@@ -141,19 +124,12 @@ describe('ProfilePage', () => {
}); });
it('renders Flag with correct props if user not active', () => { it('renders Flag with correct props if user not active', () => {
const wrapper = setup({ const userCopy = {
user: { ...globalState.user.profileUser,
user_id: 'test0',
display_name: '',
email: 'test@test.com',
github_name: 'githubName',
is_active: false, is_active: false,
manager_name: 'Test Manager', };
profile_url: 'www.test.com', const wrapper = setup({
role_name: 'Tester', user: userCopy,
slack_url: 'www.slack.com',
team_name: 'QA',
}
}).wrapper; }).wrapper;
expect(wrapper.find('#profile-title').find(Flag).props()).toMatchObject({ expect(wrapper.find('#profile-title').find(Flag).props()).toMatchObject({
caseType: 'sentenceCase', caseType: 'sentenceCase',
......
...@@ -88,7 +88,7 @@ class SearchBar extends React.Component<SearchBarProps, SearchBarState> { ...@@ -88,7 +88,7 @@ class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
render() { render() {
const subTextClass = `subtext ${this.state.subTextClassName}`; const subTextClass = `subtext ${this.state.subTextClassName}`;
return ( return (
<div id="search-bar" className="col-xs-12 col-md-offset-1 col-md-10"> <div id="search-bar">
<form className="search-bar-form" onSubmit={ this.handleValueSubmit }> <form className="search-bar-form" onSubmit={ this.handleValueSubmit }>
<input <input
id="search-input" id="search-input"
......
...@@ -150,7 +150,6 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -150,7 +150,6 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
paginationStartIndex: 0, paginationStartIndex: 0,
}; };
return ( return (
<div className="col-xs-12 col-md-offset-1 col-md-10">
<div className="search-list-container"> <div className="search-list-container">
<div className="popular-tables-header"> <div className="popular-tables-header">
<label>{POPULAR_TABLES_LABEL}</label> <label>{POPULAR_TABLES_LABEL}</label>
...@@ -158,7 +157,6 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -158,7 +157,6 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
</div> </div>
<SearchList results={ this.props.popularTables } params={ searchListParams }/> <SearchList results={ this.props.popularTables } params={ searchListParams }/>
</div> </div>
</div>
) )
}; };
...@@ -173,7 +171,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -173,7 +171,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
]; ];
return ( return (
<div className="col-xs-12 col-md-offset-1 col-md-10"> <div>
<TabsComponent <TabsComponent
tabs={ tabConfig } tabs={ tabConfig }
defaultTab={ ResourceType.table } defaultTab={ ResourceType.table }
...@@ -242,11 +240,13 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -242,11 +240,13 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
const innerContent = ( const innerContent = (
<div className="container search-page"> <div className="container search-page">
<div className="row"> <div className="row">
<div className="col-xs-12 col-md-offset-1 col-md-10">
<SearchBar handleValueSubmit={ this.onSearchBarSubmit } searchTerm={ searchTerm }/> <SearchBar handleValueSubmit={ this.onSearchBarSubmit } searchTerm={ searchTerm }/>
{ searchTerm.length > 0 && this.renderSearchResults() } { searchTerm.length > 0 && this.renderSearchResults() }
{ searchTerm.length === 0 && this.renderPopularTables() } { searchTerm.length === 0 && this.renderPopularTables() }
</div> </div>
</div> </div>
</div>
); );
if (searchTerm.length > 0) { if (searchTerm.length > 0) {
return ( return (
......
...@@ -457,17 +457,17 @@ describe('SearchPage', () => { ...@@ -457,17 +457,17 @@ describe('SearchPage', () => {
content = shallow(wrapper.instance().renderPopularTables()); content = shallow(wrapper.instance().renderPopularTables());
}); });
it('renders correct label for content', () => { it('renders correct label for content', () => {
expect(content.children().at(0).children().at(0).find('label').text()).toEqual(POPULAR_TABLES_LABEL); expect(content.children().at(0).find('label').text()).toEqual(POPULAR_TABLES_LABEL);
}); });
it('renders InfoButton with correct props', () => { it('renders InfoButton with correct props', () => {
expect(content.children().at(0).children().at(0).find(InfoButton).props()).toMatchObject({ expect(content.children().at(0).find(InfoButton).props()).toMatchObject({
infoText: POPULAR_TABLES_INFO_TEXT, infoText: POPULAR_TABLES_INFO_TEXT,
}); });
}); });
it('renders SearchList with correct props', () => { it('renders SearchList with correct props', () => {
expect(content.children().at(0).children().find(SearchList).props()).toMatchObject({ expect(content.children().find(SearchList).props()).toMatchObject({
results: props.popularTables, results: props.popularTables,
params: { params: {
source: POPULAR_TABLES_SOURCE_NAME, source: POPULAR_TABLES_SOURCE_NAME,
...@@ -483,11 +483,11 @@ describe('SearchPage', () => { ...@@ -483,11 +483,11 @@ describe('SearchPage', () => {
const content = shallow(wrapper.instance().renderSearchResults()); const content = shallow(wrapper.instance().renderSearchResults());
const expectedTabConfig = [ const expectedTabConfig = [
{ {
title: `Tables (${ props.tables.total_results })`, title: `${TABLE_RESOURCE_TITLE} (${ props.tables.total_results })`,
key: ResourceType.table, key: ResourceType.table,
content: wrapper.instance().getTabContent(props.tables, 'tables'), content: wrapper.instance().getTabContent(props.tables, TABLE_RESOURCE_TITLE),
} }
] ];
expect(content.find(TabsComponent).props()).toMatchObject({ expect(content.find(TabsComponent).props()).toMatchObject({
activeKey: wrapper.state().selectedTab, activeKey: wrapper.state().selectedTab,
defaultTab: ResourceType.table, defaultTab: ResourceType.table,
......
...@@ -22,7 +22,7 @@ class EntityCardSection extends React.Component<EntityCardSectionProps, EntityCa ...@@ -22,7 +22,7 @@ class EntityCardSection extends React.Component<EntityCardSectionProps, EntityCa
super(props); super(props);
this.state = { this.state = {
readOnly: true, readOnly: true,
} };
this.editButton = React.createRef(); this.editButton = React.createRef();
this.toggleEditMode = this.toggleEditMode.bind(this); this.toggleEditMode = this.toggleEditMode.bind(this);
......
...@@ -27,8 +27,20 @@ export function getUserById(userId: string): GetUserRequest { ...@@ -27,8 +27,20 @@ export function getUserById(userId: string): GetUserRequest {
const defaultUser = { const defaultUser = {
user_id: '',
display_name: '', display_name: '',
email: '',
employee_type: '',
first_name: '',
full_name: '',
github_username: '',
is_active: true,
last_name: '',
manager_fullname: '',
profile_url: '',
role_name: '',
slack_id: '',
team_name: '',
user_id: '',
}; };
const initialState: UserReducerState = { const initialState: UserReducerState = {
loggedInUser: defaultUser, loggedInUser: defaultUser,
......
// Setting up different types for now so we can iterate faster as shared params change // Setting up different types for now so we can iterate faster as shared params change
export interface User { export interface User {
user_id: string; email: string;
employee_type: string;
display_name: string; display_name: string;
email?: string; first_name: string;
first_name?: string; full_name: string;
github_name?: string; github_username: string;
is_active?: boolean; is_active: boolean;
last_name?: string; last_name: string;
manager_name?: string; manager_fullname: string;
profile_url?: string; profile_url: string;
role_name?: string; role_name?: string;
slack_url?: string; slack_id: string;
team_name?: string; team_name: string;
user_id: string;
} }
export type LoggedInUser = User & {}; export type LoggedInUser = User & {};
......
...@@ -106,12 +106,36 @@ const globalState: GlobalState = { ...@@ -106,12 +106,36 @@ const globalState: GlobalState = {
}, },
user: { user: {
loggedInUser: { loggedInUser: {
user_id: 'user0', display_name: 'firstname lastname',
display_name: 'User Name', email: 'test@test.com',
employee_type: 'fulltime',
first_name: 'firstname',
full_name: 'firstname lastname',
github_username: 'githubName',
is_active: true,
last_name: 'lastname',
manager_fullname: 'Test Manager',
profile_url: 'www.test.com',
role_name: 'Tester',
slack_id: 'www.slack.com',
team_name: 'QA',
user_id: 'test0',
}, },
profileUser: { profileUser: {
user_id: 'user1', display_name: 'firstname lastname',
display_name: 'User1 Name', email: 'test@test.com',
employee_type: 'fulltime',
first_name: 'firstname',
full_name: 'firstname lastname',
github_username: 'githubName',
is_active: true,
last_name: 'lastname',
manager_fullname: 'Test Manager',
profile_url: 'www.test.com',
role_name: 'Tester',
slack_id: 'www.slack.com',
team_name: 'QA',
user_id: 'test0',
}, },
}, },
}; };
......
...@@ -6,7 +6,7 @@ from http import HTTPStatus ...@@ -6,7 +6,7 @@ from http import HTTPStatus
from amundsen_application import create_app from amundsen_application import create_app
from amundsen_application.api.metadata.v0 import \ from amundsen_application.api.metadata.v0 import \
TABLE_ENDPOINT, LAST_INDEXED_ENDPOINT, POPULAR_TABLES_ENDPOINT, TAGS_ENDPOINT TABLE_ENDPOINT, LAST_INDEXED_ENDPOINT, POPULAR_TABLES_ENDPOINT, TAGS_ENDPOINT, USER_ENDPOINT
local_app = create_app('amundsen_application.config.LocalConfig') local_app = create_app('amundsen_application.config.LocalConfig')
...@@ -164,6 +164,36 @@ class MetadataTest(unittest.TestCase): ...@@ -164,6 +164,36 @@ class MetadataTest(unittest.TestCase):
"tag_name": "tag_4" "tag_name": "tag_4"
} }
] ]
self.mock_user = {
"email": "test@test.com",
"employee_type": "FTE",
"first_name": "Firstname",
"full_name": "Firstname Lastname",
"github_username": "githubusername",
"is_active": True,
"last_name": "Lastname",
"manager_fullname": "Manager Fullname",
"role_name": "SWE",
"slack_id": "slackuserid",
"team_name": "Amundsen",
"user_id": "testuserid",
}
self.expected_parsed_user = {
"display_name": "Firstname Lastname",
"email": "test@test.com",
"employee_type": "FTE",
"first_name": "Firstname",
"full_name": "Firstname Lastname",
"github_username": "githubusername",
"is_active": True,
"last_name": "Lastname",
"manager_fullname": "Manager Fullname",
"profile_url": "https://test-profile-url.com",
"role_name": "SWE",
"slack_id": "slackuserid",
"team_name": "Amundsen",
"user_id": "testuserid",
}
@responses.activate @responses.activate
def test_popular_tables_success(self) -> None: def test_popular_tables_success(self) -> None:
...@@ -475,3 +505,29 @@ class MetadataTest(unittest.TestCase): ...@@ -475,3 +505,29 @@ class MetadataTest(unittest.TestCase):
} }
) )
self.assertEquals(response.status_code, HTTPStatus.OK) self.assertEquals(response.status_code, HTTPStatus.OK)
@responses.activate
def test_get_user_failure(self) -> None:
"""
Test get_user fails when no user_id is specified
"""
url = local_app.config['METADATASERVICE_BASE'] + USER_ENDPOINT + '/testuser'
responses.add(responses.GET, url, json=self.mock_user, status=HTTPStatus.OK)
with local_app.test_client() as test:
response = test.get('/api/metadata/v0/user')
self.assertEquals(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
@responses.activate
def test_get_user_success(self) -> None:
"""
Test get_user success
"""
url = local_app.config['METADATASERVICE_BASE'] + USER_ENDPOINT + '/testuser'
responses.add(responses.GET, url, json=self.mock_user, status=HTTPStatus.OK)
with local_app.test_client() as test:
response = test.get('/api/metadata/v0/user', query_string=dict(user_id='testuser'))
data = json.loads(response.data)
self.assertEquals(response.status_code, HTTPStatus.OK)
self.assertCountEqual(data.get('user'), self.expected_parsed_user)
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