Unverified Commit 7b09a2fa authored by Daniel's avatar Daniel Committed by GitHub

Merge feature people (#71)

* Added support for UserListItems (#43)

* Added support for UserListItem
- Restyled ResourceListItem to support multiple columns
- Added timestamp for TableListItem
- Added UserListItem
- Added $text-medium and $text-light

* Cleanup and PR feedback
- Added Alumni Flag to UserListItem
- Changed usage of Flag LabelStyle to exclude the 'label-' prefix
- Added more space for 'Frequent Users' in TableListItem

* Skeleton implementation of ProfilePage w/ mock data (#44)

* Skeleton implementation w/ mock data

* Some tweaks

* Fix lint errors

* Switch profile icons to btn-flat-icon

* currentUser -> loggedInUser & profilePageUser -> profileUser

* Make a Breadcrumb component

* tweak

* Added support for UserListItems (#43)

* Added support for UserListItem
- Restyled ResourceListItem to support multiple columns
- Added timestamp for TableListItem
- Added UserListItem
- Added $text-medium and $text-light

* Cleanup and PR feedback
- Added Alumni Flag to UserListItem
- Changed usage of Flag LabelStyle to exclude the 'label-' prefix
- Added more space for 'Frequent Users' in TableListItem

* Skeleton implementation w/ mock data

* Some tweaks

* Fix lint errors

* Switch profile icons to btn-flat-icon

* currentUser -> loggedInUser & profilePageUser -> profileUser

* Make a Breadcrumb component

* tweak

* Updated search APIs to support multiple types (#48)

* Updated search APIs to support multiple types
- User search just returns mocked data

* Updated search API tests

* Add breadcrumbs + fix to tableData state management (#49)

* Update search to support multiple resource types (#51)

* Added Tabs to search results
*Split executeSearch into two actions:
- `searchAll` will search all resource types. The options allow you to search different page indexes for different resources. This is useful for loading up &selectedTab=users&tabIndex=5, since you don't necessarily want to fetch page 5 for all tabs.
- `searchResource` will run a search on a single resource type. This is primarily used for search pagination.
The URL should always reflect the current state of the search and can be refreshed or shared to maintain state.

* Improved state management with window URL and user actions
(search submit, pagination, tab change)

* changed 'last_updated' to 'last_updated_epoch' (#53)

- Conditionally hide the timestamp when not present

* Feature people design overhaul (#55)

Overhaul of UI
- NavBar - Resized to 48px, rework bottom border with box-shadow
- Search Bar - Simplify DOM elements, height 60px, font 24px bold, narrower in md, lg screens.
- Container top margin at 96, 64, 32px for lg, md, sm screens.
- Search results has 16px top and bottom
- Consolidated buttons classes to: btn-primary btn-default
- Reworked Tag buttons and labels
- Fixed panel border radiuses
- Fixed NavBar active state highlighting

* Rebase Design Overhaul from Master (#58)

* Overhaul of UI Part 1 (#54)

Overhaul of UI
- NavBar - Resized to 48px, rework bottom border with box-shadow
- Search Bar - Simplify DOM elements, height 60px, font 24px bold, narrower in md, lg screens.
- Container top margin at 96, 64, 32px for lg, md, sm screens.
- Search results has 16px top and bottom
- Consolidated buttons classes to: btn-primary btn-default
- Reworked Tag buttons and labels
- Fixed panel border radiuses
- Fixed NavBar active state highlighting

* Design Overhaul pt2 (#57)

* Design Overhaul pt2
- Standardized button colors, sizes, icons
- Text colors split into $text-dark, $text-medium, and $text-light
- Fixed some styles on search bar
- Updated TagInfo and TagInput
- Made popover colors darker

* Added support for UserListItems (#43)

* Added support for UserListItem
- Restyled ResourceListItem to support multiple columns
- Added timestamp for TableListItem
- Added UserListItem
- Added $text-medium and $text-light

* Cleanup and PR feedback
- Added Alumni Flag to UserListItem
- Changed usage of Flag LabelStyle to exclude the 'label-' prefix
- Added more space for 'Frequent Users' in TableListItem

* Skeleton implementation of ProfilePage w/ mock data (#44)

* Skeleton implementation w/ mock data

* Some tweaks

* Fix lint errors

* Switch profile icons to btn-flat-icon

* currentUser -> loggedInUser & profilePageUser -> profileUser

* Make a Breadcrumb component

* tweak

* Added support for UserListItems (#43)

* Added support for UserListItem
- Restyled ResourceListItem to support multiple columns
- Added timestamp for TableListItem
- Added UserListItem
- Added $text-medium and $text-light

* Cleanup and PR feedback
- Added Alumni Flag to UserListItem
- Changed usage of Flag LabelStyle to exclude the 'label-' prefix
- Added more space for 'Frequent Users' in TableListItem

* Skeleton implementation w/ mock data

* Some tweaks

* Fix lint errors

* Switch profile icons to btn-flat-icon

* currentUser -> loggedInUser & profilePageUser -> profileUser

* Make a Breadcrumb component

* tweak

* Updated search APIs to support multiple types (#48)

* Updated search APIs to support multiple types
- User search just returns mocked data

* Updated search API tests

* Add breadcrumbs + fix to tableData state management (#49)

* Update search to support multiple resource types (#51)

* Added Tabs to search results
*Split executeSearch into two actions:
- `searchAll` will search all resource types. The options allow you to search different page indexes for different resources. This is useful for loading up &selectedTab=users&tabIndex=5, since you don't necessarily want to fetch page 5 for all tabs.
- `searchResource` will run a search on a single resource type. This is primarily used for search pagination.
The URL should always reflect the current state of the search and can be refreshed or shared to maintain state.

* Improved state management with window URL and user actions
(search submit, pagination, tab change)

* changed 'last_updated' to 'last_updated_epoch' (#53)

- Conditionally hide the timestamp when not present

* Feature people design overhaul (#55)

Overhaul of UI
- NavBar - Resized to 48px, rework bottom border with box-shadow
- Search Bar - Simplify DOM elements, height 60px, font 24px bold, narrower in md, lg screens.
- Container top margin at 96, 64, 32px for lg, md, sm screens.
- Search results has 16px top and bottom
- Consolidated buttons classes to: btn-primary btn-default
- Reworked Tag buttons and labels
- Fixed panel border radiuses
- Fixed NavBar active state highlighting

* Fix some issues with rebasing ui overhaul from master

* Pull changes from #56 into feature/people (#60)

* Overhaul of UI Part 1 (#54)

Overhaul of UI
- NavBar - Resized to 48px, rework bottom border with box-shadow
- Search Bar - Simplify DOM elements, height 60px, font 24px bold, narrower in md, lg screens.
- Container top margin at 96, 64, 32px for lg, md, sm screens.
- Search results has 16px top and bottom
- Consolidated buttons classes to: btn-primary btn-default
- Reworked Tag buttons and labels
- Fixed panel border radiuses
- Fixed NavBar active state highlighting

* Design Overhaul pt2 (#57)

* Design Overhaul pt2
- Standardized button colors, sizes, icons
- Text colors split into $text-dark, $text-medium, and $text-light
- Fixed some styles on search bar
- Updated TagInfo and TagInput
- Made popover colors darker

* Consolidate Containers (#56)

* Consolidate un-nested containers

* Consolidate TableDetail related containers

* Consolidate FeedbackForms

* Fix merge mistakes

* Update styles

* - Disabled/Hid most Amundsen people related features
- Fixed list-group-item padding issues and border
- Adjusted column stats hover colors
- Adjusted Breadcrum styles
parent a7518e83
...@@ -553,3 +553,34 @@ def update_table_tags() -> Response: ...@@ -553,3 +553,34 @@ def update_table_tags() -> Response:
logging.exception(message) logging.exception(message)
payload = jsonify({'msg': message}) payload = jsonify({'msg': message})
return make_response(payload, HTTPStatus.INTERNAL_SERVER_ERROR) return make_response(payload, HTTPStatus.INTERNAL_SERVER_ERROR)
# TODO: Implement real support
@metadata_blueprint.route('/user', methods=['GET'])
def get_user() -> Response:
try:
user_id = get_query_param(request.args, 'user_id')
user_info = {
'first_name': 'Firstname',
'last_name': 'Lastname',
'email': 'test@test.com',
'display_name': 'Firstname Lastname',
'profile_url': 'https://github.com/lyft/amundsenfrontendlibrary',
'user_id': user_id,
'github_name': 'lyft',
'is_active': True,
'manager_name': 'Roald Amundsen',
'role_name': 'Software Engineer',
'slack_url': 'https://slack.com',
'team_name': 'Amundsen Team',
}
if user_id == 'alumni':
user_info['is_active'] = False
status_code = HTTPStatus.OK
payload = jsonify({'user': user_info})
return make_response(payload, status_code)
except Exception as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
return make_response(jsonify({'user': {}, 'msg': message}), HTTPStatus.INTERNAL_SERVER_ERROR)
...@@ -53,8 +53,8 @@ def _validate_search_term(*, search_term: str, page_index: int) -> Optional[Resp ...@@ -53,8 +53,8 @@ def _validate_search_term(*, search_term: str, page_index: int) -> Optional[Resp
return None return None
@search_blueprint.route('/', methods=['GET']) @search_blueprint.route('/table', methods=['GET'])
def search() -> Response: def search_table() -> Response:
search_term = get_query_param(request.args, 'query', 'Endpoint takes a "query" parameter') search_term = get_query_param(request.args, 'query', 'Endpoint takes a "query" parameter')
page_index = get_query_param(request.args, 'page_index', 'Endpoint takes a "page_index" parameter') page_index = get_query_param(request.args, 'page_index', 'Endpoint takes a "page_index" parameter')
...@@ -62,7 +62,20 @@ def search() -> Response: ...@@ -62,7 +62,20 @@ def search() -> Response:
if error_response is not None: if error_response is not None:
return error_response return error_response
results_dict = _search(search_term=search_term, page_index=page_index) results_dict = _search_table(search_term=search_term, page_index=page_index)
return make_response(jsonify(results_dict), results_dict.get('status_code', HTTPStatus.INTERNAL_SERVER_ERROR))
@search_blueprint.route('/user', methods=['GET'])
def search_user() -> Response:
search_term = get_query_param(request.args, 'query', 'Endpoint takes a "query" parameter')
page_index = get_query_param(request.args, 'page_index', 'Endpoint takes a "page_index" parameter')
error_response = _validate_search_term(search_term=search_term, page_index=int(page_index))
if error_response is not None:
return error_response
results_dict = _search_user(search_term=search_term, page_index=page_index)
return make_response(jsonify(results_dict), results_dict.get('status_code', HTTPStatus.INTERNAL_SERVER_ERROR)) return make_response(jsonify(results_dict), results_dict.get('status_code', HTTPStatus.INTERNAL_SERVER_ERROR))
...@@ -98,21 +111,119 @@ def _create_url_with_field(*, search_term: str, page_index: int) -> str: ...@@ -98,21 +111,119 @@ def _create_url_with_field(*, search_term: str, page_index: int) -> str:
return url return url
# TODO - Implement these functions @action_logging
def _search_tables(*, search_term: str, page_index: int) -> Dict[str, Any]: def _search_user(*, search_term: str, page_index: int) -> Dict[str, Any]:
return {} """
call the search service endpoint and return matching results
:return: a json output containing search results array as 'results'
Schema Defined Here: https://github.com/lyft/
amundsensearchlibrary/blob/master/search_service/api/search.py
def _search_dashboards(*, search_term: str, page_index: int) -> Dict[str, Any]: TODO: Define an interface for envoy_client
return {} """
def _map_user_result(result: Dict) -> Dict:
return {
'type': 'user',
'active': result.get('active', None),
'birthday': result.get('birthday', None),
'department': result.get('department', None),
'email': result.get('email', None),
'first_name': result.get('first_name', None),
'github_username': result.get('github_username', None),
'id': result.get('id', None),
'last_name': result.get('last_name', None),
'manager_email': result.get('manager_email', None),
'name': result.get('name', None),
'offboarded': result.get('offboarded', None),
'office': result.get('office', None),
'role': result.get('role', None),
'start_date': result.get('start_date', None),
'team_name': result.get('team_name', None),
'title': result.get('title', None),
}
users = {
'page_index': int(page_index),
'results': [],
'total_results': 0,
}
def _search_people(*, search_term: str, page_index: int) -> Dict[str, Any]: results_dict = {
return {} 'search_term': search_term,
'msg': 'Success',
'status_code': HTTPStatus.OK,
'users': users,
}
# TEST CODE
users['total_results'] = 3
users['results'] = [
{
'type': 'user',
'active': True,
'birthday': '10-10-2000',
'department': 'Department',
'email': 'mail@address.com',
'first_name': 'Ash',
'github_username': 'github_user',
'id': 12345,
'last_name': 'Ketchum',
'manager_email': 'manager_email',
'name': 'Ash Ketchum',
'offboarded': False,
'office': 'Kanto Region',
'role': 'Pokemon Trainer',
'start_date': '05-04-2016',
'team_name': 'Kanto Trainers',
'title': 'Pokemon Master',
},
{
'type': 'user',
'active': True,
'birthday': '06-01-2000',
'department': 'Department',
'email': 'mail@address.com',
'first_name': 'Gary',
'github_username': 'github_user',
'id': 12345,
'last_name': 'Oak',
'manager_email': 'manager_email',
'name': 'Gary Oak',
'offboarded': False,
'office': 'Kanto Region',
'role': 'Pokemon Trainer',
'start_date': '05-04-2016',
'team_name': 'Kanto Trainers',
'title': 'Pokemon Master',
},
{
'type': 'user',
'active': False,
'birthday': '06-01-60',
'department': 'Department',
'email': 'mail@address.com',
'first_name': 'Professor',
'github_username': 'github_user',
'id': 12345,
'last_name': 'Oak',
'manager_email': 'manager_email',
'name': 'Professor Oak',
'offboarded': False,
'office': 'Kanto Region',
'role': 'Scientist',
'start_date': '05-04-2016',
'team_name': 'Team Oak',
'title': 'Pokemon Researcher',
},
]
return results_dict
@action_logging @action_logging
def _search(*, search_term: str, page_index: int) -> Dict[str, Any]: def _search_table(*, search_term: str, page_index: int) -> Dict[str, Any]:
""" """
call the search service endpoint and return matching results call the search service endpoint and return matching results
:return: a json output containing search results array as 'results' :return: a json output containing search results array as 'results'
...@@ -131,7 +242,7 @@ def _search(*, search_term: str, page_index: int) -> Dict[str, Any]: ...@@ -131,7 +242,7 @@ def _search(*, search_term: str, page_index: int) -> Dict[str, Any]:
'description': result.get('description', None), 'description': result.get('description', None),
'database': result.get('database', None), 'database': result.get('database', None),
'schema_name': result.get('schema_name', None), 'schema_name': result.get('schema_name', None),
'last_updated': result.get('last_updated', None), 'last_updated_epoch': result.get('last_updated_epoch', None),
} }
tables = { tables = {
...@@ -183,3 +294,8 @@ def _search(*, search_term: str, page_index: int) -> Dict[str, Any]: ...@@ -183,3 +294,8 @@ def _search(*, search_term: str, page_index: int) -> Dict[str, Any]:
results_dict['msg'] = message results_dict['msg'] = message
logging.exception(message) logging.exception(message)
return results_dict return results_dict
# TODO - Implement
def _search_dashboard(*, search_term: str, page_index: int) -> Dict[str, Any]:
return {}
...@@ -11,11 +11,11 @@ LOGGER = logging.getLogger(__name__) ...@@ -11,11 +11,11 @@ LOGGER = logging.getLogger(__name__)
blueprint = Blueprint('api', __name__, url_prefix='/api') blueprint = Blueprint('api', __name__, url_prefix='/api')
@blueprint.route('/current_user', methods=['GET']) @blueprint.route('/auth_user', methods=['GET'])
def current_user() -> Response: def current_user() -> Response:
if (app.config['CURRENT_USER_METHOD']): if (app.config['AUTH_USER_METHOD']):
user = app.config['CURRENT_USER_METHOD'](app) user = app.config['AUTH_USER_METHOD'](app)
else: else:
user = load_user({'display_name': '*'}) user = load_user({'user_id': 'undefined', 'display_name': '*'})
return user.to_json() return user.to_json()
...@@ -39,7 +39,7 @@ class LocalConfig(Config): ...@@ -39,7 +39,7 @@ class LocalConfig(Config):
'http://{LOCAL_HOST}:{PORT}/tags'.format(LOCAL_HOST=LOCAL_HOST, PORT=METADATA_PORT) 'http://{LOCAL_HOST}:{PORT}/tags'.format(LOCAL_HOST=LOCAL_HOST, PORT=METADATA_PORT)
METADATASERVICE_REQUEST_HEADERS = None METADATASERVICE_REQUEST_HEADERS = None
CURRENT_USER_METHOD = None AUTH_USER_METHOD = None
GET_PROFILE_URL = None GET_PROFILE_URL = None
MAIL_CLIENT = None MAIL_CLIENT = None
...@@ -76,8 +76,8 @@ def _build_metrics(func_name: str, ...@@ -76,8 +76,8 @@ def _build_metrics(func_name: str,
metrics['pos_args_json'] = json.dumps(args) metrics['pos_args_json'] = json.dumps(args)
metrics['keyword_args_json'] = json.dumps(kwargs) metrics['keyword_args_json'] = json.dumps(kwargs)
if flask_app.config['CURRENT_USER_METHOD']: if flask_app.config['AUTH_USER_METHOD']:
metrics['user'] = flask_app.config['CURRENT_USER_METHOD'](flask_app).email metrics['user'] = flask_app.config['AUTH_USER_METHOD'](flask_app).email
else: else:
metrics['user'] = getpass.getuser() metrics['user'] = getpass.getuser()
......
...@@ -13,18 +13,36 @@ redesign how User handles names ...@@ -13,18 +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,
first_name: str = None, first_name: str = None,
last_name: str = None, last_name: str = None,
email: str = None, email: str = None,
display_name: str = None, display_name: str = None,
profile_url: str = None) -> 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,
slack_url: str = None,
team_name: str = None) -> None:
self.first_name = first_name self.first_name = first_name
self.last_name = last_name self.last_name = last_name
self.email = email self.email = email
self.display_name = display_name 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.slack_url = slack_url
self.team_name = team_name
# TODO: 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)
return jsonify(user_info) return jsonify(user_info)
...@@ -37,6 +55,14 @@ class UserSchema(Schema): ...@@ -37,6 +55,14 @@ class UserSchema(Schema):
display_name = fields.Str(required=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)
slack_url = fields.Str(allow_none=True)
team_name = fields.Str(allow_none=True)
@pre_load @pre_load
def generate_display_name(self, data: Dict) -> Dict: def generate_display_name(self, data: Dict) -> Dict:
if data.get('display_name', None): if data.get('display_name', None):
...@@ -67,6 +93,8 @@ class UserSchema(Schema): ...@@ -67,6 +93,8 @@ class UserSchema(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('One or more must be provided: "first_name", "last_name", "email", "display_name"')
if not data.get('user_id', None):
raise ValidationError('"user_id" must be provided')
def load_user(user_data: Dict) -> User: def load_user(user_data: Dict) -> User:
......
...@@ -31,6 +31,11 @@ img.icon { ...@@ -31,6 +31,11 @@ img.icon {
mask-image: url('/static/images/icons/Down.svg'); mask-image: url('/static/images/icons/Down.svg');
} }
&.icon-github {
-webkit-mask-image: url('/static/images/icons/github.svg');
mask-image: url('/static/images/icons/github.svg');
}
&.icon-left { &.icon-left {
-webkit-mask-image: url('/static/images/icons/Left.svg'); -webkit-mask-image: url('/static/images/icons/Left.svg');
mask-image: url('/static/images/icons/Left.svg'); mask-image: url('/static/images/icons/Left.svg');
...@@ -41,6 +46,11 @@ img.icon { ...@@ -41,6 +46,11 @@ img.icon {
mask-image: url('/static/images/icons/Loader.svg'); mask-image: url('/static/images/icons/Loader.svg');
} }
&.icon-mail {
-webkit-mask-image: url('/static/images/icons/mail.svg');
mask-image: url('/static/images/icons/mail.svg');
}
&.icon-plus-circle { &.icon-plus-circle {
-webkit-mask-image: url('/static/images/icons/Plus-Circle.svg'); -webkit-mask-image: url('/static/images/icons/Plus-Circle.svg');
mask-image: url('/static/images/icons/Plus-Circle.svg'); mask-image: url('/static/images/icons/Plus-Circle.svg');
...@@ -61,10 +71,20 @@ img.icon { ...@@ -61,10 +71,20 @@ img.icon {
mask-image: url('/static/images/icons/Search.svg'); mask-image: url('/static/images/icons/Search.svg');
} }
&.icon-slack {
-webkit-mask-image: url('/static/images/icons/slack.svg');
mask-image: url('/static/images/icons/slack.svg');
}
&.icon-up { &.icon-up {
-webkit-mask-image: url('/static/images/icons/Up.svg'); -webkit-mask-image: url('/static/images/icons/Up.svg');
mask-image: url('/static/images/icons/Up.svg'); mask-image: url('/static/images/icons/Up.svg');
} }
&.icon-users {
-webkit-mask-image: url('/static/images/icons/users.svg');
mask-image: url('/static/images/icons/users.svg');
}
} }
.disabled, .disabled,
......
...@@ -6,8 +6,7 @@ ...@@ -6,8 +6,7 @@
border-left: none; border-left: none;
border-right: none; border-right: none;
cursor: pointer; cursor: pointer;
padding-left: 4px; padding: 0;
padding-right: 4px;
&:hover { &:hover {
border-color: $gray; border-color: $gray;
......
...@@ -4,4 +4,3 @@ ...@@ -4,4 +4,3 @@
@import 'variables-custom'; @import 'variables-custom';
// Bootstrap Default Values // Bootstrap Default Values
@import '~bootstrap-sass/assets/stylesheets/bootstrap/variables'; @import '~bootstrap-sass/assets/stylesheets/bootstrap/variables';
...@@ -35,7 +35,6 @@ form { ...@@ -35,7 +35,6 @@ form {
margin-bottom: 0; margin-bottom: 0;
} }
input { input {
&::-webkit-input-placeholder, &::-webkit-input-placeholder,
&::-moz-placeholder, &::-moz-placeholder,
...@@ -46,3 +45,8 @@ input { ...@@ -46,3 +45,8 @@ input {
} }
} }
.truncated {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mail"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-slack"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"></path><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"></path><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"></path><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"></path><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z"></path><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"></path><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z"></path><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z"></path></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
\ No newline at end of file
import * as React from 'react'; import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Avatar from 'react-avatar'; import Avatar from 'react-avatar';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withRouter } from 'react-router-dom' import { withRouter } from 'react-router-dom'
import AppConfig from '../../../config/config'; import AppConfig from '../../../config/config';
import { GlobalState } from "../../ducks/rootReducer"; import { GlobalState } from "../../ducks/rootReducer";
import { executeSearch } from '../../ducks/search/reducer'; import { getLoggedInUser } from "../../ducks/user/reducer";
import { getPopularTables } from '../../ducks/popularTables/reducer'; import { LoggedInUser, GetLoggedInUserRequest } from "../../ducks/user/types";
import { getCurrentUser } from "../../ducks/user/reducer";
import { CurrentUser, GetCurrentUserRequest } from "../../ducks/user/types";
import './styles.scss'; import './styles.scss';
// Props // Props
interface StateFromProps { interface StateFromProps {
currentUser: CurrentUser; loggedInUser: LoggedInUser;
} }
interface DispatchFromProps { interface DispatchFromProps {
getCurrentUser: () => GetCurrentUserRequest; getLoggedInUser: () => GetLoggedInUserRequest;
} }
type NavBarProps = StateFromProps & DispatchFromProps; type NavBarProps = StateFromProps & DispatchFromProps;
// State // State
interface NavBarState { interface NavBarState {
currentUser: CurrentUser; loggedInUser: LoggedInUser;
} }
export class NavBar extends React.Component<NavBarProps, NavBarState> { export class NavBar extends React.Component<NavBarProps, NavBarState> {
...@@ -36,17 +33,17 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> { ...@@ -36,17 +33,17 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> {
super(props); super(props);
this.state = { this.state = {
currentUser: this.props.currentUser, loggedInUser: this.props.loggedInUser,
}; };
} }
static getDerivedStateFromProps(nextProps, prevState) { static getDerivedStateFromProps(nextProps, prevState) {
const { currentUser } = nextProps; const { loggedInUser } = nextProps;
return { currentUser }; return { loggedInUser };
} }
componentDidMount() { componentDidMount() {
this.props.getCurrentUser(); this.props.getLoggedInUser();
} }
render() { render() {
...@@ -73,8 +70,11 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> { ...@@ -73,8 +70,11 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> {
}) })
} }
{ {
this.state.currentUser && this.state.loggedInUser &&
<Avatar name={this.state.currentUser.display_name} size={32} round={true}/> // TODO PEOPLE - Uncomment when enabling people
//<Link to={`/user/${this.state.loggedInUser.user_id}`}>
<Avatar name={this.state.loggedInUser.display_name} size={32} round={true} />
//</Link>
} }
</div> </div>
</div> </div>
...@@ -86,12 +86,12 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> { ...@@ -86,12 +86,12 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> {
export const mapStateToProps = (state: GlobalState) => { export const mapStateToProps = (state: GlobalState) => {
return { return {
currentUser: state.user.currentUser, loggedInUser: state.user.loggedInUser,
} }
}; };
export const mapDispatchToProps = (dispatch) => { const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ getCurrentUser }, dispatch); return bindActionCreators({ getLoggedInUser }, dispatch);
}; };
export default withRouter(connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(NavBar)); export default withRouter(connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(NavBar));
...@@ -4,11 +4,13 @@ import * as DocumentTitle from 'react-document-title'; ...@@ -4,11 +4,13 @@ import * as DocumentTitle from 'react-document-title';
// TODO: Use css-modules instead of 'import' // TODO: Use css-modules instead of 'import'
import './styles.scss'; import './styles.scss';
import Breadcrumb from "../common/Breadcrumb";
const NotFoundPage: React.SFC<any> = () => { const NotFoundPage: React.SFC<any> = () => {
return ( return (
<DocumentTitle title="404 Page Not Found - Amundsen"> <DocumentTitle title="404 Page Not Found - Amundsen">
<div className="not-found-page"> <div className="container not-found-page">
<Breadcrumb path='/' text='Home'/>
<h1>404 Page Not Found</h1> <h1>404 Page Not Found</h1>
<span className="glyphicon glyphicon-exclamation-sign" /> <span className="glyphicon glyphicon-exclamation-sign" />
</div> </div>
......
@import 'variables'; @import 'variables';
.not-found-page { .not-found-page {
width: 70%; h1 {
text-align: center; text-align: center;
}
span {
text-align: center;
width: 100%;
}
} }
.glyphicon.glyphicon-exclamation-sign { .glyphicon.glyphicon-exclamation-sign {
......
import * as React from 'react';
import * as DocumentTitle from 'react-document-title';
import Avatar from 'react-avatar';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import LoadingSpinner from '../common/LoadingSpinner';
import { GlobalState } from "../../ducks/rootReducer";
import { getUserById } from "../../ducks/user/reducer";
import { LoggedInUser, GetUserRequest } from "../../ducks/user/types";
import Breadcrumb from "../common/Breadcrumb";
import Flag from "../common/Flag";
import Tabs from "../common/Tabs";
import './styles.scss';
interface StateFromProps {
user: LoggedInUser;
}
interface DispatchFromProps {
getUserById: (userId: string) => GetUserRequest;
}
type ProfilePageProps = StateFromProps & DispatchFromProps;
interface ProfilePageState {
user: LoggedInUser;
}
class ProfilePage extends React.Component<ProfilePageProps, ProfilePageState> {
private userId: string;
constructor(props) {
super(props);
const { match } = props;
const params = match.params;
this.userId = params ? params.userId : '';
this.state = {
user: this.props.user,
};
}
static getDerivedStateFromProps(nextProps, prevState) {
const { user } = nextProps;
return { user };
}
componentDidMount() {
this.props.getUserById(this.userId);
}
// TODO: consider moving logic for empty content into Tab component
createEmptyTabMessage = (message: string) => {
return (
<div className="empty-tab-message">
<text>{ message }</text>
</div>
);
}
generateTabInfo = () => {
const user = this.state.user;
const tabInfo = [];
// TODO: Populate tabs based on data
// TODO: consider moving logic for empty content into Tab component
tabInfo.push({
content: this.createEmptyTabMessage('User has no frequently used resources.'),
key: 'frequentUses_tab',
title: 'Frequently Uses (0)',
});
tabInfo.push({
content: this.createEmptyTabMessage('User has no bookmarked resources.'),
key: 'bookmarks_tab',
title: 'Bookmarks (0)',
});
tabInfo.push({
content: this.createEmptyTabMessage('User has no owned resources.'),
key: 'owner_tab',
title: 'Owner (0)',
});
return tabInfo;
}
/* 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 */
render() {
const user = this.state.user;
return (
<DocumentTitle title={ `${user.display_name} - Amundsen Profile` }>
<div className="container profile-page">
<Breadcrumb path='/' text='Search Results'/>
<div className="profile-header">
<div 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 className="profile-title">
<h1>{ user.display_name }</h1>
{
(user.is_active === false) &&
<Flag caseType="sentenceCase" labelStyle="label-danger" text="Alumni"/>
}
</div>
<text>{ `${user.role_name} on ${user.team_name}` }</text>
<text>{ `Manager: ${user.manager_name}` }</text>
<div className="profile-icons">
{
user.is_active &&
<a href={user.slack_url} className='btn btn-flat-icon' target='_blank'>
<img className='icon icon-slack'/>
<span>Slack</span>
</a>
}
{
user.is_active &&
<a href={`mailto:${user.email}`} className='btn btn-flat-icon' target='_blank'>
<img className='icon icon-mail'/>
<span>{ user.email }</span>
</a>
}
{
user.is_active &&
<a href={user.profile_url} className='btn btn-flat-icon' target='_blank'>
<img className='icon icon-users'/>
<span>Employee Profile</span>
</a>
}
<a href={`https://github.com/${user.github_name}`} className='btn btn-flat-icon' target='_blank'>
<img className='icon icon-github'/>
<span>Github</span>
</a>
</div>
</div>
</div>
<div className="profile-tabs">
<Tabs tabs={ this.generateTabInfo() } defaultTab='frequentUses_tab' />
</div>
</div>
</DocumentTitle>
);
}
}
const mapStateToProps = (state: GlobalState) => {
return {
user: state.user.profileUser,
}
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ getUserById }, dispatch);
};
export default connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(ProfilePage);
@import 'variables';
.profile-page {
margin-bottom: 48px;
}
.profile-header {
display: flex;
.profile-avatar {
margin-left: 24px;
margin-right: 24px;
}
.profile-details {
display: flex;
flex-direction: column;
h1 {
margin-bottom: 0px;
margin-top: 0px;
}
.profile-icons {
margin-top: 12px;
a {
margin-left: 8px;
margin-right: 8px;
&:first-child {
margin-left: 0px;
}
&:last-child {
margin-right: 0px;
}
}
}
.profile-title {
display: flex;
margin-bottom: 8px;
.flag {
height: min-content; // TODO: consider moving height into Flag component
font-size: 100%;
margin: auto auto auto 12px;
}
}
}
}
.profile-tabs {
margin-top: 48px;
}
// TODO: consider moving logic for empty content into Tab component
.empty-tab-message {
color: $gray-light;
margin-top: 64px;
text-align: center;
}
...@@ -7,7 +7,7 @@ const DEFAULT_SUBTEXT = `Search within a category using the pattern with wildcar ...@@ -7,7 +7,7 @@ const DEFAULT_SUBTEXT = `Search within a category using the pattern with wildcar
Current categories are 'column', 'schema', 'table', and 'tag'.`; Current categories are 'column', 'schema', 'table', and 'tag'.`;
interface SearchBarProps { interface SearchBarProps {
handleValueSubmit: (term: string, pageIndex: number) => void; handleValueSubmit: (term: string) => void;
placeholder?: string; placeholder?: string;
searchTerm?: string; searchTerm?: string;
subText?: string; subText?: string;
...@@ -50,7 +50,7 @@ class SearchBar extends React.Component<SearchBarProps, SearchBarState> { ...@@ -50,7 +50,7 @@ class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
event.preventDefault(); event.preventDefault();
if (this.isFormValid()) { if (this.isFormValid()) {
const inputElement = this.inputRef.current; const inputElement = this.inputRef.current;
this.props.handleValueSubmit(inputElement.value.toLowerCase(), 0); this.props.handleValueSubmit(inputElement.value.toLowerCase());
} }
}; };
......
...@@ -5,11 +5,11 @@ ...@@ -5,11 +5,11 @@
position: relative; position: relative;
.search-bar-button { .search-bar-button {
height: 24px;
left: 24px;
position: absolute; position: absolute;
top: 28px; height: 24px;
width: 24px; width: 24px;
top: 28px;
left: 24px;
} }
.search-bar-input { .search-bar-input {
......
@import 'variables'; @import 'variables';
.search-page { .search-page {
.tabs-component,
.search-list-container { .search-list-container {
margin: 64px 0 0 0; margin-top: 32px;
} }
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
.tabs-component,
.search-list-container { .search-list-container {
margin: 32px 0 0 0; margin-top: 16px;
} }
} }
.search-list-header { .popular-tables-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-bottom: 32px; margin-bottom: 32px;
label {
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
font-size: 20px;
margin-top: auto;
margin-bottom: auto;
width: fit-content;
line-height: 24px;
}
}
.tab-content {
.search-list-header {
label {
color: $text-medium;
font-size: 18px;
}
}
.list-group-item:first-child {
border-top: none;
}
} }
.search-list-header label {
font-family: $font-family-sans-serif-bold; .search-error {
font-weight: $font-weight-sans-serif-bold; color: $gray-lighter;
font-size: 20px; text-align: center;
margin-top: auto;
margin-bottom: auto;
width: fit-content;
line-height: 24px;
} }
.search-pagination-component { .search-pagination-component {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
}
.pagination { .pagination > li {
} > a,
.pagination>li>a, > span {
.pagination>li>span { border: 1px solid $gray-lighter;
color: $brand-color-4; color: $brand-color-4;
border: 1px solid $gray-lighter;
} &:focus,
.pagination>.active>a, &:hover {
.pagination>.active>a:hover, background-color: $gray-lighter;
.pagination>.active>a:focus, color: $link-hover-color;
.pagination>.active>span, z-index: 0;
.pagination>.active>span:hover, }
.pagination>.active>span:focus { }
z-index: 0;
background-color: $brand-color-4; &.active {
border-color: $brand-color-4 > a,
} > span {
.pagination>li>a:focus, &,
.pagination>li>a:hover, &:active,
.pagination>li>span:focus, &:hover,
.pagination>li>span:hover { &:focus {
z-index: 0; background-color: $brand-color-4;
color: $link-hover-color; border-color: $brand-color-4;
background-color: $gray-lighter; color: white;
z-index: 0;
}
}
}
}
} }
import * as React from 'react'; import * as React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Button, Modal, OverlayTrigger, Popover, Table } from 'react-bootstrap'; import { Button, Modal, OverlayTrigger, Popover, Table } from 'react-bootstrap';
import Linkify from 'react-linkify' import Linkify from 'react-linkify'
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
border-top-color: $gray-lighter !important; border-top-color: $gray-lighter !important;
border-bottom-color: $gray-lighter !important; border-bottom-color: $gray-lighter !important;
background-color: transparent !important; background-color: transparent !important;
padding: 10px 4px;
.description { .description {
color: $text-medium; color: $text-medium;
...@@ -54,7 +55,7 @@ ...@@ -54,7 +55,7 @@
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-image: linear-gradient($brand-color-1, $brand-color-1, white); background-image: linear-gradient($gray-lighter, $gray-lighter, white);
.type { .type {
color: $brand-color-4; color: $brand-color-4;
......
...@@ -3,7 +3,6 @@ import { connect } from 'react-redux'; ...@@ -3,7 +3,6 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import * as DocumentTitle from 'react-document-title'; import * as DocumentTitle from 'react-document-title';
import * as $ from 'jquery';
import * as qs from 'simple-query-string'; import * as qs from 'simple-query-string';
import { GlobalState } from "../../ducks/rootReducer"; import { GlobalState } from "../../ducks/rootReducer";
...@@ -12,6 +11,7 @@ import { GetTableDataRequest } from '../../ducks/tableMetadata/types'; ...@@ -12,6 +11,7 @@ import { GetTableDataRequest } from '../../ducks/tableMetadata/types';
import AppConfig from '../../../config/config'; import AppConfig from '../../../config/config';
import AvatarLabel from '../common/AvatarLabel'; import AvatarLabel from '../common/AvatarLabel';
import Breadcrumb from "../common/Breadcrumb";
import DataPreviewButton from './DataPreviewButton'; import DataPreviewButton from './DataPreviewButton';
import DetailList from './DetailList'; import DetailList from './DetailList';
import EntityCard from '../common/EntityCard'; import EntityCard from '../common/EntityCard';
...@@ -21,8 +21,6 @@ import TableDescEditableText from './TableDescEditableText'; ...@@ -21,8 +21,6 @@ import TableDescEditableText from './TableDescEditableText';
import TagInput from '../Tags/TagInput'; import TagInput from '../Tags/TagInput';
import WatermarkLabel from "./WatermarkLabel"; import WatermarkLabel from "./WatermarkLabel";
import { Tag } from '../Tags/types';
import Avatar from 'react-avatar'; import Avatar from 'react-avatar';
import { OverlayTrigger, Popover } from 'react-bootstrap'; import { OverlayTrigger, Popover } from 'react-bootstrap';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
...@@ -304,13 +302,15 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone ...@@ -304,13 +302,15 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone
this.props.history.push('/404'); this.props.history.push('/404');
} else if (this.state.statusCode === 500) { } else if (this.state.statusCode === 500) {
innerContent = ( innerContent = (
<div className="error-label"> <div className="container error-label">
<Breadcrumb path='/' text='Search Results'/>
<label className="d-block m-auto">Something went wrong...</label> <label className="d-block m-auto">Something went wrong...</label>
</div> </div>
) )
} else { } else {
innerContent = ( innerContent = (
<div className="container table-detail"> <div className="container table-detail">
<Breadcrumb path='/' text='Search Results'/>
<div className="row"> <div className="row">
<div className="detail-header col-xs-12 col-md-7 col-lg-8"> <div className="detail-header col-xs-12 col-md-7 col-lg-8">
<div className="title">{ `${data.schema}.${data.table_name}` }</div> <div className="title">{ `${data.schema}.${data.table_name}` }</div>
......
import * as React from 'react';
import { Link } from 'react-router-dom';
import './styles.scss';
interface BreadcrumbProps {
path: string;
text: string;
}
const Breadcrumb: React.SFC<BreadcrumbProps> = ({ path, text }) => {
return (
<div className="amundsen-breadcrumb">
<Link to={path}>
<button className='btn btn-flat-icon'>
<img className='icon icon-left'/>
<span>{text}</span>
</button>
</Link>
</div>
);
};
Breadcrumb.defaultProps = {
path: '/',
text: 'Home',
};
export default 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 {
color: $text-medium;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
height: 24px;
margin-top: -72px;
margin-bottom: 48px;
img.icon-left {
margin: -3px 0 -3px -8px;
}
span {
display: inline-block;
height: 24px;
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;
}
}
...@@ -32,14 +32,14 @@ const Flag: React.SFC<FlagProps> = ({ caseType, text, labelStyle }) => { ...@@ -32,14 +32,14 @@ const Flag: React.SFC<FlagProps> = ({ caseType, text, labelStyle }) => {
// TODO: After upgrading to Bootstrap 4, this component should leverage badges // TODO: After upgrading to Bootstrap 4, this component should leverage badges
// https://getbootstrap.com/docs/4.1/components/badge/ // https://getbootstrap.com/docs/4.1/components/badge/
return ( return (
<span className={`flag label ${labelStyle}`}>{convertText(text, caseType)}</span> <span className={`flag label label-${labelStyle}`}>{convertText(text, caseType)}</span>
); );
}; };
Flag.defaultProps = { Flag.defaultProps = {
caseType: null, caseType: null,
text: '', text: '',
labelStyle: 'label-default', labelStyle: 'default',
}; };
export default Flag; export default Flag;
...@@ -3,10 +3,8 @@ import { Link } from 'react-router-dom'; ...@@ -3,10 +3,8 @@ import { Link } from 'react-router-dom';
import { LoggingParams, TableResource} from '../types'; import { LoggingParams, TableResource} from '../types';
import './styles.scss';
interface TableListItemProps { interface TableListItemProps {
item: TableResource; table: TableResource;
logging: LoggingParams; logging: LoggingParams;
} }
...@@ -15,38 +13,44 @@ class TableListItem extends React.Component<TableListItemProps, {}> { ...@@ -15,38 +13,44 @@ class TableListItem extends React.Component<TableListItemProps, {}> {
super(props); super(props);
} }
/* TODO: We have to fix a bug with this feature. Commented out support.
const createLastUpdatedTimestamp = () => {
if (lastUpdated) {
const dateTokens = new Date(lastUpdated).toDateString().split(' ');
return (
<label>
{`${dateTokens[1].toUpperCase()} ${dateTokens[2]}`}
</label>
)
}
return null;
}*/
getLink = () => { getLink = () => {
const { item, logging } = this.props; const { table, logging } = this.props;
return `/table_detail/${item.cluster}/${item.database}/${item.schema_name}/${item.name}` return `/table_detail/${table.cluster}/${table.database}/${table.schema_name}/${table.name}`
+ `?index=${logging.index}&source=${logging.source}`; + `?index=${logging.index}&source=${logging.source}`;
}; };
render() { render() {
const { item } = this.props; const { table } = this.props;
const hasLastUpdated = !!table.last_updated_epoch;
const dateTokens = new Date(table.last_updated_epoch * 1000).toDateString().split(' ');
const dateLabel = `${dateTokens[1]} ${dateTokens[2]}, ${dateTokens[3]}`;
return ( return (
<li className="list-group-item search-list-item"> <li className="list-group-item">
<Link to={ this.getLink() }> <Link className="resource-list-item table-list-item" to={ this.getLink() }>
<img className="icon icon-color icon-database" /> <img className="icon icon-database icon-color" />
<div className="resultInfo"> <div className="content">
<span className="title truncated">{ `${item.schema_name}.${item.name} `}</span> <div className={ hasLastUpdated? "col-sm-9 col-md-10" : "col-sm-12"}>
<span className="subtitle truncated">{ item.description }</span> <div className="main-title truncated">{ `${table.schema_name}.${table.name}`}</div>
<div className="description truncated">{ table.description }</div>
</div>
{/*<div className={ hasLastUpdated? "hidden-xs col-sm-3 col-md-4" : "hidden-xs col-sm-6"}>*/}
{/*<div className="secondary-title">Frequent Users</div>*/}
{/*<div className="description truncated">*/}
{/*<label> </label>*/}
{/*</div>*/}
{/*</div>*/}
{
hasLastUpdated &&
<div className="hidden-xs col-sm-3 col-md-2">
<div className="secondary-title">Latest Data</div>
<div className="description truncated">
{ dateLabel }
</div>
</div>
}
</div> </div>
{ /*createLastUpdatedTimestamp()*/ }
<img className="icon icon-right" /> <img className="icon icon-right" />
</Link> </Link>
</li> </li>
......
@import 'variables';
.search-list-item {
padding: 16px 4px;
.resultInfo {
margin-left: 8px;
margin-right: 8px;
width: 100%;
min-width: 0px;
}
.title {
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
color: $text-dark;
font-size: 16px;
}
.subtitle {
color: $text-medium;
font-size: 14px;
}
.truncated {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
label {
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
font-size: 12px;
color: $text-medium;
margin-top: auto;
margin-bottom: auto;
white-space: nowrap;
}
a {
text-decoration: none;
color: $text-dark;
display: flex;
flex-direction: row;
}
img.icon {
margin: 8px;
}
&:hover img.icon {
background-color: $brand-color-4;
}
}
import * as React from 'react';
import Avatar from 'react-avatar';
import { Link } from 'react-router-dom';
import { LoggingParams, UserResource} from '../types';
import Flag from '../../Flag';
interface UserListItemProps {
user: UserResource;
logging: LoggingParams;
}
class UserListItem extends React.Component<UserListItemProps, {}> {
constructor(props) {
super(props);
}
getLink = () => {
const { user, logging } = this.props;
return `/user/${user.id}/?index=${logging.index}&source=${logging.source}`;
};
render() {
const { user } = this.props;
return (
<li className="list-group-item">
<Link className="resource-list-item user-list-item" to={ this.getLink() }>
<Avatar name={ user.name } size={ 24 } round={ true } />
<div className="content">
<div className="col-xs-12 col-sm-6">
<div className="main-title">
{ user.name }
{
!user.active &&
<Flag text="Alumni" labelStyle='danger' />
}
</div>
<div className="description">
{ `${user.role} on ${user.team_name}` }
</div>
</div>
<div className="hidden-xs col-sm-6">
<div className="secondary-title">Frequently Uses</div>
<div className="description truncated">
{ /*TODO Fill this with a real value*/ }
<label>{ user.title }</label>
</div>
</div>
</div>
<img className="icon icon-right" />
</Link>
</li>
);
}
}
export default UserListItem;
import * as React from 'react' import * as React from 'react'
import { LoggingParams, Resource, ResourceType, TableResource } from './types'; import { LoggingParams, Resource, ResourceType, TableResource, UserResource } from './types';
import TableListItem from './TableListItem'; import TableListItem from './TableListItem';
import UserListItem from './UserListItem';
import './styles.scss';
interface ListItemProps { interface ListItemProps {
logging: LoggingParams; logging: LoggingParams;
...@@ -16,9 +20,10 @@ export default class ResourceListItem extends React.Component<ListItemProps, {}> ...@@ -16,9 +20,10 @@ export default class ResourceListItem extends React.Component<ListItemProps, {}>
render() { render() {
switch(this.props.item.type) { switch(this.props.item.type) {
case ResourceType.table: case ResourceType.table:
return (<TableListItem item={ this.props.item as TableResource } logging={ this.props.logging } />); return (<TableListItem table={ this.props.item as TableResource } logging={ this.props.logging } />);
// case ListItemType.user: case ResourceType.user:
// case ListItemType.dashboard: return (<UserListItem user={ this.props.item as UserResource } logging={ this.props.logging } />);
// case ResourceType.dashboard:
default: default:
return (null); return (null);
} }
......
@import 'variables';
.list-group-item .resource-list-item {
color: $text-dark;
display: flex;
flex-direction: row;
height: 78px;
padding: 16px 8px;
text-decoration: none;
img.icon,
.sb-avatar {
margin: auto 0;
}
&:hover img.icon {
background-color: $brand-color-4;
}
.content {
width: 100%;
min-width: 0; /* Needed to support `white-space: nowrap` */
.main-title {
color: $text-dark;
font-size: $font-size-large;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
}
.secondary-title {
color: $text-medium;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
}
.description {
color: $text-medium;
display: block;
font-size: $font-size-base;
margin-top: 2px;
> a {
color: $brand-color-4;
&:hover {
color: $brand-color-5;
}
}
}
}
}
...@@ -10,21 +10,69 @@ export interface Resource { ...@@ -10,21 +10,69 @@ export interface Resource {
export interface TableResource extends Resource { export interface TableResource extends Resource {
type: ResourceType.table; type: ResourceType.table;
database: string;
cluster: string; cluster: string;
database: string;
description: string; description: string;
key: string; key: string;
last_updated: number; // 'popular_tables' currently does not support 'last_updated_epoch'
last_updated_epoch?: number;
name: string; name: string;
schema_name: string; schema_name: string;
} }
/**
* This is a sample of the user data type which includes all fields.
* We will only need a subset of this for UserResource.
interface User {
active : boolean;
backupCodes: any[]; // Not sure of type
birthday : string | null;
department: string;
department_id: string;
email: string;
employment_type: string;
first_name: string;
github_username: string;
hris_active: boolean;
hris_number: string;
hris_source : string;
id: number;
last_name: string;
manager_email : string;
manager_id: number;
manager_hris_number: string;
mobile_phone : string | null;
name : string;
offboarded : boolean;
office: string;
role: string;
start_date : string;
team_name: string;
title: string;
work_phone: string;
}
*/
// Placeholder until the schema is defined. // Placeholder until the schema is defined.
export interface UserResource extends Resource { export interface UserResource extends Resource {
type: ResourceType.user; type: ResourceType.user;
active : boolean;
birthday : string | null;
department: string;
email: string;
first_name: string; first_name: string;
github_username: string;
id: number;
last_name: string; last_name: string;
email: string; manager_email : string;
name : string;
offboarded : boolean;
office: string;
role: string;
start_date : string;
team_name: string;
title: string;
} }
// Placeholder until the schema is defined. // Placeholder until the schema is defined.
......
...@@ -5,7 +5,7 @@ import './styles.scss'; ...@@ -5,7 +5,7 @@ import './styles.scss';
export interface TabsProps { export interface TabsProps {
tabs: TabInfo[]; tabs: TabInfo[];
activeKey?: string;
defaultTab?: string; defaultTab?: string;
onSelect?: (key: string) => void; onSelect?: (key: string) => void;
} }
...@@ -16,12 +16,13 @@ interface TabInfo { ...@@ -16,12 +16,13 @@ interface TabInfo {
title: string; title: string;
} }
const TabsComponent: React.SFC<TabsProps> = ({tabs, defaultTab, onSelect}) => { const TabsComponent: React.SFC<TabsProps> = ({tabs, activeKey, defaultTab, onSelect}) => {
return ( return (
<Tabs <Tabs
id="tab" id="tab"
className="tabs-component" className="tabs-component"
defaultActiveKey={ defaultTab } defaultActiveKey={ defaultTab }
activeKey={ activeKey }
onSelect={ onSelect } onSelect={ onSelect }
> >
{ {
......
@import 'variables'; @import 'variables';
.tabs-component { .tabs-component .nav.nav-tabs {
.nav.nav-tabs { border: none;
border: none;
> li { > li {
&.active > a { &.active > a {
&, &,
&:hover { &:hover {
color: $brand-color-4; color: $brand-color-4;
} }
&:after { &:after {
opacity: 1; opacity: 1;
}
} }
}
> a { > a {
background: none; background: none;
border: none; border: none;
color: $text-medium; color: $text-medium;
font-size: $font-size-large; font-size: $font-size-large;
line-height: $line-height-large; line-height: $line-height-large;
padding: 4px 8px 12px;
&:hover { &:hover {
color: $text-dark; color: $text-dark;
} }
// Active tab indicator // Active tab indicator
&:after { &:after {
border: 2px solid $brand-color-4; border: 2px solid $brand-color-4;
bottom: 0; bottom: 0;
content: ""; content: "";
left: 0; left: 0;
opacity: 0; opacity: 0;
position: absolute; position: absolute;
transition: opacity 200ms ease-in; transition: opacity 200ms ease-in;
width: 100%; width: 100%;
}
} }
} }
} }
......
...@@ -8,7 +8,7 @@ import { submitFeedbackWatcher } from './feedback/sagas'; ...@@ -8,7 +8,7 @@ import { submitFeedbackWatcher } from './feedback/sagas';
// SearchPage // SearchPage
import { getPopularTablesWatcher } from './popularTables/sagas'; import { getPopularTablesWatcher } from './popularTables/sagas';
import { executeSearchWatcher } from './search/sagas'; import { searchAllWatcher, searchResourceWatcher } from './search/sagas';
// TableDetail // TableDetail
import { updateTableOwnerWatcher } from './tableMetadata/owners/sagas'; import { updateTableOwnerWatcher } from './tableMetadata/owners/sagas';
...@@ -27,7 +27,7 @@ import { ...@@ -27,7 +27,7 @@ import {
import { getAllTagsWatcher } from './allTags/sagas'; import { getAllTagsWatcher } from './allTags/sagas';
// User // User
import { getCurrentUserWatcher } from "./user/sagas"; import { getLoggedInUserWatcher, getUserWatcher } from "./user/sagas";
export default function* rootSaga() { export default function* rootSaga() {
yield all([ yield all([
...@@ -36,7 +36,8 @@ export default function* rootSaga() { ...@@ -36,7 +36,8 @@ export default function* rootSaga() {
// FeedbackForm // FeedbackForm
submitFeedbackWatcher(), submitFeedbackWatcher(),
// SearchPage // SearchPage
executeSearchWatcher(), searchAllWatcher(),
searchResourceWatcher(),
getPopularTablesWatcher(), getPopularTablesWatcher(),
// Tags // Tags
getAllTagsWatcher(), getAllTagsWatcher(),
...@@ -51,6 +52,7 @@ export default function* rootSaga() { ...@@ -51,6 +52,7 @@ export default function* rootSaga() {
updateTableOwnerWatcher(), updateTableOwnerWatcher(),
updateTableTagsWatcher(), updateTableTagsWatcher(),
// User // User
getCurrentUserWatcher(), getLoggedInUserWatcher(),
getUserWatcher(),
]); ]);
} }
import axios, { AxiosResponse, AxiosError } from 'axios'; import axios, { AxiosError, AxiosResponse } from 'axios';
import { SearchResponse } from '../types'; import { SearchAllRequest, SearchResponse, SearchResourceRequest } from '../types';
import { SearchReducerState } from '../reducer'; const BASE_URL = '/api/search/v0';
function transformSearchResults(data: SearchResponse): SearchReducerState {
return {
searchTerm: data.search_term,
dashboards: data.dashboards,
tables: data.tables,
users: data.users,
};
}
export function searchExecuteSearch(action) { export function searchAll(action: SearchAllRequest) {
const { term, pageIndex } = action; const { term, options } = action;
return axios.get(`/api/search/v0/?query=${term}&page_index=${pageIndex}`) return axios.all([
.then((response: AxiosResponse<SearchResponse>) => transformSearchResults(response.data)) axios.get(`${BASE_URL}/table?query=${term}&page_index=${options.tableIndex || 0}`),
.catch((error: AxiosError) => { // TODO PEOPLE - Uncomment when enabling People feature
const data = error.response ? error.response.data : {}; // axios.get(`${BASE_URL}/user?query=${term}&page_index=${options.userIndex || 0}`),
return transformSearchResults(data); ]).then(axios.spread((tableResponse: AxiosResponse<SearchResponse> /*, userResponse: AxiosResponse<SearchResponse>*/) => {
return {
search_term: tableResponse.data.search_term,
tables: tableResponse.data.tables,
// users: userResponse.data.users,
}
})).catch((error: AxiosError) => {
// TODO - handle errors
}); });
} }
export function searchResource(action: SearchResourceRequest) {
const { term, pageIndex, resource } = action;
return axios.get(`${BASE_URL}/${resource}?query=${term}&page_index=${pageIndex}`)
.then((response: AxiosResponse) => {
const { data } = response;
const ret = { searchTerm: data.search_term };
['tables', 'users'].forEach((key) => {
if (data[key]) {
ret[key] = data[key];
}
});
return ret;
}).catch((error: AxiosError) => {
// TODO - handle errors
});
}
import { import {
ExecuteSearch, SearchAll,
ExecuteSearchRequest, SearchAllOptions,
ExecuteSearchResponse, SearchAllRequest,
SearchAllResponse,
SearchResource,
SearchResourceRequest,
SearchResourceResponse,
DashboardSearchResults, DashboardSearchResults,
TableSearchResults, TableSearchResults,
UserSearchResults, UserSearchResults,
} from './types'; } from './types';
import { ResourceType } from "../../components/common/ResourceListItem/types";
export type SearchReducerAction = ExecuteSearchRequest | ExecuteSearchResponse; export type SearchReducerAction = SearchAllResponse | SearchResourceResponse;
export interface SearchReducerState { export interface SearchReducerState {
searchTerm: string; search_term: string;
dashboards: DashboardSearchResults; dashboards: DashboardSearchResults;
tables: TableSearchResults; tables: TableSearchResults;
users: UserSearchResults; users: UserSearchResults;
} }
export function executeSearch(term: string, pageIndex: number): ExecuteSearchRequest { export function searchAll(term: string, options: SearchAllOptions = {}): SearchAllRequest {
return { return {
options,
term, term,
type: SearchAll.ACTION,
};
}
export function searchResource(resource: ResourceType, term: string, pageIndex: number): SearchResourceRequest {
return {
pageIndex, pageIndex,
type: ExecuteSearch.ACTION, term,
resource,
type: SearchResource.ACTION,
}; };
} }
const initialState: SearchReducerState = { const initialState: SearchReducerState = {
searchTerm: '', search_term: '',
dashboards: { dashboards: {
page_index: 0, page_index: 0,
results: [], results: [],
...@@ -44,10 +58,22 @@ const initialState: SearchReducerState = { ...@@ -44,10 +58,22 @@ const initialState: SearchReducerState = {
}; };
export default function reducer(state: SearchReducerState = initialState, action: SearchReducerAction): SearchReducerState { export default function reducer(state: SearchReducerState = initialState, action: SearchReducerAction): SearchReducerState {
let newState = action.payload;
switch (action.type) { switch (action.type) {
case ExecuteSearch.SUCCESS: // SearchAll will reset all resources with search results or the initial state
return action.payload; case SearchAll.SUCCESS:
case ExecuteSearch.FAILURE: return {
...initialState,
...newState,
};
// SearchResource will set only a single resource and preserves search state for other resources
case SearchResource.SUCCESS:
return {
...state,
...newState,
};
case SearchAll.FAILURE:
case SearchResource.FAILURE:
return initialState; return initialState;
default: default:
return state; return state;
......
...@@ -2,24 +2,42 @@ import { call, put, takeEvery } from 'redux-saga/effects'; ...@@ -2,24 +2,42 @@ import { call, put, takeEvery } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga'; import { SagaIterator } from 'redux-saga';
import { import {
ExecuteSearch, SearchAll,
ExecuteSearchRequest, SearchAllRequest,
SearchResource,
SearchResourceRequest,
} from './types'; } from './types';
import { import {
searchExecuteSearch, searchAll, searchResource,
} from './api/v0'; } from './api/v0';
export function* executeSearchWorker(action: ExecuteSearchRequest): SagaIterator { // SearchAll
export function* searchAllWorker(action: SearchAllRequest): SagaIterator {
try { try {
const searchResults = yield call(searchExecuteSearch, action); const searchResults = yield call(searchAll, action);
yield put({ type: ExecuteSearch.SUCCESS, payload: searchResults }); yield put({ type: SearchAll.SUCCESS, payload: searchResults });
} catch (e) { } catch (e) {
yield put({ type: ExecuteSearch.FAILURE }); yield put({ type: SearchAll.FAILURE });
} }
} }
export function* executeSearchWatcher(): SagaIterator { export function* searchAllWatcher(): SagaIterator {
yield takeEvery(ExecuteSearch.ACTION, executeSearchWorker); yield takeEvery(SearchAll.ACTION, searchAllWorker);
}
// SearchResource
export function* searchResourceWorker(action: SearchResourceRequest): SagaIterator {
try {
const searchResults = yield call(searchResource, action);
yield put({ type: SearchResource.SUCCESS, payload: searchResults });
} catch (e) {
yield put({ type: SearchResource.FAILURE });
}
}
export function* searchResourceWatcher(): SagaIterator {
yield takeEvery(SearchResource.ACTION, searchResourceWorker);
} }
import { Resource, DashboardResource, TableResource, UserResource } from "../../components/common/ResourceListItem/types"; import {
Resource,
ResourceType,
DashboardResource,
TableResource,
UserResource,
} from "../../components/common/ResourceListItem/types";
import { SearchReducerState } from './reducer'; import { SearchReducerState } from './reducer';
interface SearchResults<T extends Resource> { interface SearchResults<T extends Resource> {
...@@ -14,25 +20,51 @@ export type SearchResponse = { ...@@ -14,25 +20,51 @@ export type SearchResponse = {
msg: string; msg: string;
status_code: number; status_code: number;
search_term: string; search_term: string;
dashboards: DashboardSearchResults; dashboards?: DashboardSearchResults;
tables: TableSearchResults; tables?: TableSearchResults;
users: UserSearchResults; users?: UserSearchResults;
} }
/* executeSearch */ /* searchAll - Search all resource types */
export enum ExecuteSearch { export enum SearchAll {
ACTION = 'amundsen/search/EXECUTE_SEARCH', ACTION = 'amundsen/search/SEARCH_ALL',
SUCCESS = 'amundsen/search/EXECUTE_SEARCH_SUCCESS', SUCCESS = 'amundsen/search/SEARCH_ALL_SUCCESS',
FAILURE = 'amundsen/search/EXECUTE_SEARCH_FAILURE', FAILURE = 'amundsen/search/SEARCH_ALL_FAILURE',
} }
export interface ExecuteSearchRequest { export interface SearchAllOptions {
type: ExecuteSearch.ACTION; dashboardIndex?: number;
tableIndex?: number;
userIndex?: number;
}
export interface SearchAllRequest {
options: SearchAllOptions;
term: string; term: string;
type: SearchAll.ACTION;
}
export interface SearchAllResponse {
type: SearchAll.SUCCESS | SearchAll.FAILURE;
payload?: SearchReducerState;
}
/* searchResource - Search a single resource type */
export enum SearchResource {
ACTION = 'amundsen/search/SEARCH_RESOURCE',
SUCCESS = 'amundsen/search/SEARCH_RESOURCE_SUCCESS',
FAILURE = 'amundsen/search/SEARCH_RESOURCE_FAILURE',
}
export interface SearchResourceRequest {
pageIndex: number; pageIndex: number;
resource: ResourceType;
term: string;
type: SearchResource.ACTION;
} }
export interface ExecuteSearchResponse { export interface SearchResourceResponse {
type: ExecuteSearch.SUCCESS | ExecuteSearch.FAILURE; type: SearchResource.SUCCESS | SearchResource.FAILURE;
payload?: SearchReducerState; payload?: SearchReducerState;
} }
...@@ -129,10 +129,20 @@ export default function reducer(state: TableMetadataReducerState = initialState, ...@@ -129,10 +129,20 @@ export default function reducer(state: TableMetadataReducerState = initialState,
...state, ...state,
isLoading: true, isLoading: true,
preview: initialPreviewState, preview: initialPreviewState,
tableData: initialTableDataState,
tableOwners: tableOwnersReducer(state.tableOwners, action), tableOwners: tableOwnersReducer(state.tableOwners, action),
tableTags: tableTagsReducer(state.tableTags, action), tableTags: tableTagsReducer(state.tableTags, action),
}; };
case GetTableData.FAILURE: case GetTableData.FAILURE:
return {
...state,
isLoading: false,
preview: initialPreviewState,
statusCode: action.payload.statusCode,
tableData: initialTableDataState,
tableOwners: tableOwnersReducer(state.tableOwners, action),
tableTags: tableTagsReducer(state.tableTags, action),
};
case GetTableData.SUCCESS: case GetTableData.SUCCESS:
return { return {
...state, ...state,
...@@ -148,12 +158,13 @@ export default function reducer(state: TableMetadataReducerState = initialState, ...@@ -148,12 +158,13 @@ export default function reducer(state: TableMetadataReducerState = initialState,
case GetColumnDescription.FAILURE: case GetColumnDescription.FAILURE:
case GetColumnDescription.SUCCESS: case GetColumnDescription.SUCCESS:
return { ...state, tableData: action.payload }; return { ...state, tableData: action.payload };
case GetLastIndexed.SUCCESS:
return { ...state, lastIndexed: action.payload };
case GetLastIndexed.FAILURE: case GetLastIndexed.FAILURE:
return { ...state, lastIndexed: null }; return { ...state, lastIndexed: null };
case GetPreviewData.SUCCESS: case GetLastIndexed.SUCCESS:
return { ...state, lastIndexed: action.payload };
case GetPreviewData.FAILURE: case GetPreviewData.FAILURE:
return { ...state, preview: initialPreviewState };
case GetPreviewData.SUCCESS:
return { ...state, preview: action.payload }; return { ...state, preview: action.payload };
case UpdateTableOwner.ACTION: case UpdateTableOwner.ACTION:
case UpdateTableOwner.FAILURE: case UpdateTableOwner.FAILURE:
......
...@@ -24,10 +24,10 @@ import { ...@@ -24,10 +24,10 @@ import {
// getTableData // getTableData
export function* getTableDataWorker(action: GetTableDataRequest): SagaIterator { export function* getTableDataWorker(action: GetTableDataRequest): SagaIterator {
try { try {
const { data, owners, tags } = yield call(metadataGetTableData, action); const { data, owners, statusCode, tags } = yield call(metadataGetTableData, action);
yield put({ type: GetTableData.SUCCESS, payload: { data, owners, tags } }); yield put({ type: GetTableData.SUCCESS, payload: { data, owners, statusCode, tags } });
} catch (e) { } catch (e) {
yield put({ type: GetTableData.FAILURE, payload: { data: {}, owners: [], tags: [] } }); yield put({ type: GetTableData.FAILURE, payload: { data: {}, owners: [], statusCode: 500, tags: [] } });
} }
} }
......
import axios, { AxiosResponse, AxiosError } from 'axios'; import axios, { AxiosResponse, AxiosError } from 'axios';
import { CurrentUser } from '../types'; import { LoggedInUser, UserResponse } from '../types';
export function getCurrentUser() { export function getLoggedInUser() {
return axios.get(`/api/current_user`) return axios.get(`/api/auth_user`)
.then((response: AxiosResponse<CurrentUser>) => { .then((response: AxiosResponse<LoggedInUser>) => {
return response.data; return response.data;
}).catch((error: AxiosError) => { }).catch((error: AxiosError) => {
return {}; return {};
}); });
} }
export function getUserById(userId: string) {
return axios.get(`/api/metadata/v0/user?user_id=${userId}`)
.then((response: AxiosResponse<UserResponse>) => {
return response.data.user;
})
.catch((error: AxiosError) => {
return {};
});
}
import { import {
GetCurrentUser, GetLoggedInUser,
GetCurrentUserRequest, GetLoggedInUserRequest,
GetCurrentUserResponse, GetLoggedInUserResponse,
CurrentUser GetUser,
GetUserRequest,
GetUserResponse,
LoggedInUser, User
} from './types'; } from './types';
type UserReducerAction = GetCurrentUserRequest | GetCurrentUserResponse; type UserReducerAction =
GetLoggedInUserRequest | GetLoggedInUserResponse |
GetUserRequest | GetUserResponse ;
export interface UserReducerState { export interface UserReducerState {
currentUser: CurrentUser; loggedInUser: LoggedInUser;
profileUser: User;
} }
export function getCurrentUser(): GetCurrentUserRequest { export function getLoggedInUser(): GetLoggedInUserRequest {
return { type: GetCurrentUser.ACTION }; return { type: GetLoggedInUser.ACTION };
} }
export function getUserById(userId: string): GetUserRequest {
return { userId, type: GetUser.ACTION };
}
const defaultUser = {
user_id: '',
display_name: '',
};
const initialState: UserReducerState = { const initialState: UserReducerState = {
currentUser: null, loggedInUser: defaultUser,
profileUser: defaultUser,
}; };
export default function reducer(state: UserReducerState = initialState, action: UserReducerAction): UserReducerState { export default function reducer(state: UserReducerState = initialState, action: UserReducerAction): UserReducerState {
switch (action.type) { switch (action.type) {
case GetCurrentUser.SUCCESS: case GetLoggedInUser.SUCCESS:
return { ...state, currentUser: action.payload }; return { ...state, loggedInUser: action.payload };
case GetUser.ACTION:
case GetUser.FAILURE:
return { ...state, profileUser: defaultUser };
case GetUser.SUCCESS:
return { ...state, profileUser: action.payload };
default: default:
return state; return state;
} }
......
import { SagaIterator } from 'redux-saga'; import { SagaIterator } from 'redux-saga';
import { call, put, takeEvery } from 'redux-saga/effects'; import { call, put, takeEvery } from 'redux-saga/effects';
import { GetCurrentUser } from './types'; import { GetLoggedInUser, GetUser, GetUserRequest } from './types';
import { getCurrentUser } from './api/v0'; import { getLoggedInUser, getUserById } from './api/v0';
export function* getUserWorker(): SagaIterator { export function* getLoggedInUserWorker(): SagaIterator {
try { try {
const user = yield call(getCurrentUser); const user = yield call(getLoggedInUser);
yield put({ type: GetCurrentUser.SUCCESS, payload: user }); const otherUserInfo = yield call(getUserById, user.user_id);
yield put({ type: GetLoggedInUser.SUCCESS, payload: { ...otherUserInfo, ...user }});
} catch (e) { } catch (e) {
yield put({ type: GetCurrentUser.FAILURE }); yield put({ type: GetLoggedInUser.FAILURE });
} }
} }
export function* getCurrentUserWatcher(): SagaIterator { export function* getLoggedInUserWatcher(): SagaIterator {
yield takeEvery(GetCurrentUser.ACTION, getUserWorker); yield takeEvery(GetLoggedInUser.ACTION, getLoggedInUserWorker);
}
export function* getUserWorker(action: GetUserRequest): SagaIterator {
try {
const user = yield call(getUserById, action.userId);
yield put({ type: GetUser.SUCCESS, payload: user });
} catch (e) {
yield put({ type: GetUser.FAILURE});
}
}
export function* getUserWatcher(): SagaIterator {
yield takeEvery(GetUser.ACTION, getUserWorker);
} }
export interface CurrentUser { // Setting up different types for now so we can iterate faster as shared params change
export interface User {
user_id: string;
display_name: string; display_name: string;
email: string; email?: string;
first_name: string; first_name?: string;
last_name: string; github_name?: string;
profile_url: string; is_active?: boolean;
last_name?: string;
manager_name?: string;
profile_url?: string;
role_name?: string;
slack_url?: string;
team_name?: string;
} }
export type LoggedInUser = User & {};
export enum GetCurrentUser { export type UserResponse = { user: User; msg: string; };
/* getLoggedInUser */
export enum GetLoggedInUser {
ACTION = 'amundsen/current_user/GET_ACTION',
SUCCESS = 'amundsen/current_user/GET_SUCCESS',
FAILURE = 'amundsen/current_user/GET_FAILURE',
}
export interface GetLoggedInUserRequest {
type: GetLoggedInUser.ACTION;
}
export interface GetLoggedInUserResponse {
type: GetLoggedInUser.SUCCESS | GetLoggedInUser.FAILURE;
payload?: LoggedInUser;
}
/* getUserById */
export enum GetUser {
ACTION = 'amundsen/user/GET_ACTION', ACTION = 'amundsen/user/GET_ACTION',
SUCCESS = 'amundsen/user/GET_SUCCESS', SUCCESS = 'amundsen/user/GET_SUCCESS',
FAILURE = 'amundsen/user/GET_FAILURE', FAILURE = 'amundsen/user/GET_FAILURE',
} }
export interface GetCurrentUserRequest { export interface GetUserRequest {
type: GetCurrentUser.ACTION; type: GetUser.ACTION;
userId: string;
} }
export interface GetCurrentUserResponse { export interface GetUserResponse {
type: GetCurrentUser.SUCCESS | GetCurrentUser.FAILURE; type: GetUser.SUCCESS | GetUser.FAILURE;
payload?: CurrentUser; payload?: User;
} }
...@@ -15,6 +15,7 @@ import Feedback from './components/Feedback'; ...@@ -15,6 +15,7 @@ import Feedback from './components/Feedback';
import Footer from './components/Footer'; import Footer from './components/Footer';
import NavBar from './components/NavBar'; import NavBar from './components/NavBar';
import NotFoundPage from './components/NotFoundPage'; import NotFoundPage from './components/NotFoundPage';
import ProfilePage from './components/ProfilePage';
import SearchPage from './components/SearchPage'; import SearchPage from './components/SearchPage';
import TableDetail from './components/TableDetail'; import TableDetail from './components/TableDetail';
...@@ -38,6 +39,7 @@ ReactDOM.render( ...@@ -38,6 +39,7 @@ ReactDOM.render(
<Route path="/announcements" component={AnnouncementPage} /> <Route path="/announcements" component={AnnouncementPage} />
<Route path="/browse" component={BrowsePage} /> <Route path="/browse" component={BrowsePage} />
<Route path="/search" component={SearchPage} /> <Route path="/search" component={SearchPage} />
<Route path="/user/:userId" component={ProfilePage} />
<Route path="/404" component={NotFoundPage} /> <Route path="/404" component={NotFoundPage} />
<Route path="/" component={SearchPage} /> <Route path="/" component={SearchPage} />
</Switch> </Switch>
......
...@@ -66,7 +66,6 @@ class MetadataTest(unittest.TestCase): ...@@ -66,7 +66,6 @@ class MetadataTest(unittest.TestCase):
'id': 'test_id', 'id': 'test_id',
'description': 'This is a test' 'description': 'This is a test'
}, },
'last_updated_timestamp': 1534191754
} }
self.expected_parsed_metadata = { self.expected_parsed_metadata = {
'cluster': 'test_cluster', 'cluster': 'test_cluster',
......
This diff is collapsed.
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