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:
logging.exception(message)
payload = jsonify({'msg': message})
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
return None
@search_blueprint.route('/', methods=['GET'])
def search() -> Response:
@search_blueprint.route('/table', methods=['GET'])
def search_table() -> 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')
......@@ -62,7 +62,20 @@ def search() -> Response:
if error_response is not None:
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))
......@@ -98,21 +111,119 @@ def _create_url_with_field(*, search_term: str, page_index: int) -> str:
return url
# TODO - Implement these functions
def _search_tables(*, search_term: str, page_index: int) -> Dict[str, Any]:
return {}
@action_logging
def _search_user(*, search_term: str, page_index: int) -> Dict[str, Any]:
"""
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]:
return {}
TODO: Define an interface for envoy_client
"""
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),
}
def _search_people(*, search_term: str, page_index: int) -> Dict[str, Any]:
return {}
users = {
'page_index': int(page_index),
'results': [],
'total_results': 0,
}
results_dict = {
'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
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
: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]:
'description': result.get('description', None),
'database': result.get('database', 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 = {
......@@ -183,3 +294,8 @@ def _search(*, search_term: str, page_index: int) -> Dict[str, Any]:
results_dict['msg'] = message
logging.exception(message)
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__)
blueprint = Blueprint('api', __name__, url_prefix='/api')
@blueprint.route('/current_user', methods=['GET'])
@blueprint.route('/auth_user', methods=['GET'])
def current_user() -> Response:
if (app.config['CURRENT_USER_METHOD']):
user = app.config['CURRENT_USER_METHOD'](app)
if (app.config['AUTH_USER_METHOD']):
user = app.config['AUTH_USER_METHOD'](app)
else:
user = load_user({'display_name': '*'})
user = load_user({'user_id': 'undefined', 'display_name': '*'})
return user.to_json()
......@@ -39,7 +39,7 @@ class LocalConfig(Config):
'http://{LOCAL_HOST}:{PORT}/tags'.format(LOCAL_HOST=LOCAL_HOST, PORT=METADATA_PORT)
METADATASERVICE_REQUEST_HEADERS = None
CURRENT_USER_METHOD = None
AUTH_USER_METHOD = None
GET_PROFILE_URL = None
MAIL_CLIENT = None
......@@ -76,8 +76,8 @@ def _build_metrics(func_name: str,
metrics['pos_args_json'] = json.dumps(args)
metrics['keyword_args_json'] = json.dumps(kwargs)
if flask_app.config['CURRENT_USER_METHOD']:
metrics['user'] = flask_app.config['CURRENT_USER_METHOD'](flask_app).email
if flask_app.config['AUTH_USER_METHOD']:
metrics['user'] = flask_app.config['AUTH_USER_METHOD'](flask_app).email
else:
metrics['user'] = getpass.getuser()
......
......@@ -13,18 +13,36 @@ redesign how User handles names
class User:
# TODO: alphabetize after we have the real params
def __init__(self,
first_name: str = None,
last_name: str = None,
email: 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.last_name = last_name
self.email = email
self.display_name = display_name
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:
user_info = dump_user(self)
return jsonify(user_info)
......@@ -37,6 +55,14 @@ class UserSchema(Schema):
display_name = fields.Str(required=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
def generate_display_name(self, data: Dict) -> Dict:
if data.get('display_name', None):
......@@ -67,6 +93,8 @@ class UserSchema(Schema):
def validate_user(self, data: Dict) -> None:
if not data.get('display_name', None):
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:
......
......@@ -31,6 +31,11 @@ img.icon {
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 {
-webkit-mask-image: url('/static/images/icons/Left.svg');
mask-image: url('/static/images/icons/Left.svg');
......@@ -41,6 +46,11 @@ img.icon {
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 {
-webkit-mask-image: url('/static/images/icons/Plus-Circle.svg');
mask-image: url('/static/images/icons/Plus-Circle.svg');
......@@ -61,10 +71,20 @@ img.icon {
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 {
-webkit-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,
......
......@@ -6,8 +6,7 @@
border-left: none;
border-right: none;
cursor: pointer;
padding-left: 4px;
padding-right: 4px;
padding: 0;
&:hover {
border-color: $gray;
......
......@@ -4,4 +4,3 @@
@import 'variables-custom';
// Bootstrap Default Values
@import '~bootstrap-sass/assets/stylesheets/bootstrap/variables';
......@@ -35,7 +35,6 @@ form {
margin-bottom: 0;
}
input {
&::-webkit-input-placeholder,
&::-moz-placeholder,
......@@ -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 { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Avatar from 'react-avatar';
import { Link, NavLink } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { withRouter } from 'react-router-dom'
import AppConfig from '../../../config/config';
import { GlobalState } from "../../ducks/rootReducer";
import { executeSearch } from '../../ducks/search/reducer';
import { getPopularTables } from '../../ducks/popularTables/reducer';
import { getCurrentUser } from "../../ducks/user/reducer";
import { CurrentUser, GetCurrentUserRequest } from "../../ducks/user/types";
import { getLoggedInUser } from "../../ducks/user/reducer";
import { LoggedInUser, GetLoggedInUserRequest } from "../../ducks/user/types";
import './styles.scss';
// Props
interface StateFromProps {
currentUser: CurrentUser;
loggedInUser: LoggedInUser;
}
interface DispatchFromProps {
getCurrentUser: () => GetCurrentUserRequest;
getLoggedInUser: () => GetLoggedInUserRequest;
}
type NavBarProps = StateFromProps & DispatchFromProps;
// State
interface NavBarState {
currentUser: CurrentUser;
loggedInUser: LoggedInUser;
}
export class NavBar extends React.Component<NavBarProps, NavBarState> {
......@@ -36,17 +33,17 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> {
super(props);
this.state = {
currentUser: this.props.currentUser,
loggedInUser: this.props.loggedInUser,
};
}
static getDerivedStateFromProps(nextProps, prevState) {
const { currentUser } = nextProps;
return { currentUser };
const { loggedInUser } = nextProps;
return { loggedInUser };
}
componentDidMount() {
this.props.getCurrentUser();
this.props.getLoggedInUser();
}
render() {
......@@ -73,8 +70,11 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> {
})
}
{
this.state.currentUser &&
<Avatar name={this.state.currentUser.display_name} size={32} round={true}/>
this.state.loggedInUser &&
// 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>
......@@ -86,12 +86,12 @@ export class NavBar extends React.Component<NavBarProps, NavBarState> {
export const mapStateToProps = (state: GlobalState) => {
return {
currentUser: state.user.currentUser,
loggedInUser: state.user.loggedInUser,
}
};
export const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ getCurrentUser }, dispatch);
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ getLoggedInUser }, dispatch);
};
export default withRouter(connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(NavBar));
......@@ -4,11 +4,13 @@ import * as DocumentTitle from 'react-document-title';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
import Breadcrumb from "../common/Breadcrumb";
const NotFoundPage: React.SFC<any> = () => {
return (
<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>
<span className="glyphicon glyphicon-exclamation-sign" />
</div>
......
@import 'variables';
.not-found-page {
width: 70%;
h1 {
text-align: center;
}
span {
text-align: center;
width: 100%;
}
}
.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
Current categories are 'column', 'schema', 'table', and 'tag'.`;
interface SearchBarProps {
handleValueSubmit: (term: string, pageIndex: number) => void;
handleValueSubmit: (term: string) => void;
placeholder?: string;
searchTerm?: string;
subText?: string;
......@@ -50,7 +50,7 @@ class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
event.preventDefault();
if (this.isFormValid()) {
const inputElement = this.inputRef.current;
this.props.handleValueSubmit(inputElement.value.toLowerCase(), 0);
this.props.handleValueSubmit(inputElement.value.toLowerCase());
}
};
......
......@@ -5,11 +5,11 @@
position: relative;
.search-bar-button {
height: 24px;
left: 24px;
position: absolute;
top: 28px;
height: 24px;
width: 24px;
top: 28px;
left: 24px;
}
.search-bar-input {
......
@import 'variables';
.search-page {
.tabs-component,
.search-list-container {
margin: 64px 0 0 0;
margin-top: 32px;
}
@media (max-width: $screen-sm-max) {
.tabs-component,
.search-list-container {
margin: 32px 0 0 0;
margin-top: 16px;
}
}
.search-list-header {
.popular-tables-header {
display: flex;
flex-direction: row;
margin-bottom: 32px;
}
.search-list-header label {
label {
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
font-size: 20px;
......@@ -24,34 +28,59 @@
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-error {
color: $gray-lighter;
text-align: center;
}
.search-pagination-component {
display: flex;
justify-content: center;
}
}
.pagination {
}
.pagination>li>a,
.pagination>li>span {
color: $brand-color-4;
.pagination > li {
> a,
> span {
border: 1px solid $gray-lighter;
}
.pagination>.active>a,
.pagination>.active>a:hover,
.pagination>.active>a:focus,
.pagination>.active>span,
.pagination>.active>span:hover,
.pagination>.active>span:focus {
color: $brand-color-4;
&:focus,
&:hover {
background-color: $gray-lighter;
color: $link-hover-color;
z-index: 0;
}
}
&.active {
> a,
> span {
&,
&:active,
&:hover,
&:focus {
background-color: $brand-color-4;
border-color: $brand-color-4
}
.pagination>li>a:focus,
.pagination>li>a:hover,
.pagination>li>span:focus,
.pagination>li>span:hover {
border-color: $brand-color-4;
color: white;
z-index: 0;
color: $link-hover-color;
background-color: $gray-lighter;
}
}
}
}
}
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Button, Modal, OverlayTrigger, Popover, Table } from 'react-bootstrap';
import Linkify from 'react-linkify'
......
......@@ -9,6 +9,7 @@
border-top-color: $gray-lighter !important;
border-bottom-color: $gray-lighter !important;
background-color: transparent !important;
padding: 10px 4px;
.description {
color: $text-medium;
......@@ -54,7 +55,7 @@
cursor: pointer;
&:hover {
background-image: linear-gradient($brand-color-1, $brand-color-1, white);
background-image: linear-gradient($gray-lighter, $gray-lighter, white);
.type {
color: $brand-color-4;
......
......@@ -3,7 +3,6 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as DocumentTitle from 'react-document-title';
import * as $ from 'jquery';
import * as qs from 'simple-query-string';
import { GlobalState } from "../../ducks/rootReducer";
......@@ -12,6 +11,7 @@ import { GetTableDataRequest } from '../../ducks/tableMetadata/types';
import AppConfig from '../../../config/config';
import AvatarLabel from '../common/AvatarLabel';
import Breadcrumb from "../common/Breadcrumb";
import DataPreviewButton from './DataPreviewButton';
import DetailList from './DetailList';
import EntityCard from '../common/EntityCard';
......@@ -21,8 +21,6 @@ import TableDescEditableText from './TableDescEditableText';
import TagInput from '../Tags/TagInput';
import WatermarkLabel from "./WatermarkLabel";
import { Tag } from '../Tags/types';
import Avatar from 'react-avatar';
import { OverlayTrigger, Popover } from 'react-bootstrap';
import { RouteComponentProps } from 'react-router';
......@@ -304,13 +302,15 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone
this.props.history.push('/404');
} else if (this.state.statusCode === 500) {
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>
</div>
)
} else {
innerContent = (
<div className="container table-detail">
<Breadcrumb path='/' text='Search Results'/>
<div className="row">
<div className="detail-header col-xs-12 col-md-7 col-lg-8">
<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 }) => {
// TODO: After upgrading to Bootstrap 4, this component should leverage badges
// https://getbootstrap.com/docs/4.1/components/badge/
return (
<span className={`flag label ${labelStyle}`}>{convertText(text, caseType)}</span>
<span className={`flag label label-${labelStyle}`}>{convertText(text, caseType)}</span>
);
};
Flag.defaultProps = {
caseType: null,
text: '',
labelStyle: 'label-default',
labelStyle: 'default',
};
export default Flag;
......@@ -3,10 +3,8 @@ import { Link } from 'react-router-dom';
import { LoggingParams, TableResource} from '../types';
import './styles.scss';
interface TableListItemProps {
item: TableResource;
table: TableResource;
logging: LoggingParams;
}
......@@ -15,38 +13,44 @@ class TableListItem extends React.Component<TableListItemProps, {}> {
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 = () => {
const { item, logging } = this.props;
return `/table_detail/${item.cluster}/${item.database}/${item.schema_name}/${item.name}`
const { table, logging } = this.props;
return `/table_detail/${table.cluster}/${table.database}/${table.schema_name}/${table.name}`
+ `?index=${logging.index}&source=${logging.source}`;
};
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 (
<li className="list-group-item search-list-item">
<Link to={ this.getLink() }>
<img className="icon icon-color icon-database" />
<div className="resultInfo">
<span className="title truncated">{ `${item.schema_name}.${item.name} `}</span>
<span className="subtitle truncated">{ item.description }</span>
<li className="list-group-item">
<Link className="resource-list-item table-list-item" to={ this.getLink() }>
<img className="icon icon-database icon-color" />
<div className="content">
<div className={ hasLastUpdated? "col-sm-9 col-md-10" : "col-sm-12"}>
<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>
{ /*createLastUpdatedTimestamp()*/ }
<img className="icon icon-right" />
</Link>
</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 { LoggingParams, Resource, ResourceType, TableResource } from './types';
import { LoggingParams, Resource, ResourceType, TableResource, UserResource } from './types';
import TableListItem from './TableListItem';
import UserListItem from './UserListItem';
import './styles.scss';
interface ListItemProps {
logging: LoggingParams;
......@@ -16,9 +20,10 @@ export default class ResourceListItem extends React.Component<ListItemProps, {}>
render() {
switch(this.props.item.type) {
case ResourceType.table:
return (<TableListItem item={ this.props.item as TableResource } logging={ this.props.logging } />);
// case ListItemType.user:
// case ListItemType.dashboard:
return (<TableListItem table={ this.props.item as TableResource } logging={ this.props.logging } />);
case ResourceType.user:
return (<UserListItem user={ this.props.item as UserResource } logging={ this.props.logging } />);
// case ResourceType.dashboard:
default:
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 {
export interface TableResource extends Resource {
type: ResourceType.table;
database: string;
cluster: string;
database: string;
description: string;
key: string;
last_updated: number;
// 'popular_tables' currently does not support 'last_updated_epoch'
last_updated_epoch?: number;
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.
export interface UserResource extends Resource {
type: ResourceType.user;
active : boolean;
birthday : string | null;
department: string;
email: string;
first_name: string;
github_username: string;
id: number;
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.
......
......@@ -5,7 +5,7 @@ import './styles.scss';
export interface TabsProps {
tabs: TabInfo[];
activeKey?: string;
defaultTab?: string;
onSelect?: (key: string) => void;
}
......@@ -16,12 +16,13 @@ interface TabInfo {
title: string;
}
const TabsComponent: React.SFC<TabsProps> = ({tabs, defaultTab, onSelect}) => {
const TabsComponent: React.SFC<TabsProps> = ({tabs, activeKey, defaultTab, onSelect}) => {
return (
<Tabs
id="tab"
className="tabs-component"
defaultActiveKey={ defaultTab }
activeKey={ activeKey }
onSelect={ onSelect }
>
{
......
@import 'variables';
.tabs-component {
.nav.nav-tabs {
.tabs-component .nav.nav-tabs {
border: none;
> li {
......@@ -22,6 +21,7 @@
color: $text-medium;
font-size: $font-size-large;
line-height: $line-height-large;
padding: 4px 8px 12px;
&:hover {
color: $text-dark;
......@@ -40,5 +40,4 @@
}
}
}
}
}
......@@ -8,7 +8,7 @@ import { submitFeedbackWatcher } from './feedback/sagas';
// SearchPage
import { getPopularTablesWatcher } from './popularTables/sagas';
import { executeSearchWatcher } from './search/sagas';
import { searchAllWatcher, searchResourceWatcher } from './search/sagas';
// TableDetail
import { updateTableOwnerWatcher } from './tableMetadata/owners/sagas';
......@@ -27,7 +27,7 @@ import {
import { getAllTagsWatcher } from './allTags/sagas';
// User
import { getCurrentUserWatcher } from "./user/sagas";
import { getLoggedInUserWatcher, getUserWatcher } from "./user/sagas";
export default function* rootSaga() {
yield all([
......@@ -36,7 +36,8 @@ export default function* rootSaga() {
// FeedbackForm
submitFeedbackWatcher(),
// SearchPage
executeSearchWatcher(),
searchAllWatcher(),
searchResourceWatcher(),
getPopularTablesWatcher(),
// Tags
getAllTagsWatcher(),
......@@ -51,6 +52,7 @@ export default function* rootSaga() {
updateTableOwnerWatcher(),
updateTableTagsWatcher(),
// 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 {
export function searchAll(action: SearchAllRequest) {
const { term, options } = action;
return axios.all([
axios.get(`${BASE_URL}/table?query=${term}&page_index=${options.tableIndex || 0}`),
// TODO PEOPLE - Uncomment when enabling People feature
// axios.get(`${BASE_URL}/user?query=${term}&page_index=${options.userIndex || 0}`),
]).then(axios.spread((tableResponse: AxiosResponse<SearchResponse> /*, userResponse: AxiosResponse<SearchResponse>*/) => {
return {
searchTerm: data.search_term,
dashboards: data.dashboards,
tables: data.tables,
users: data.users,
};
search_term: tableResponse.data.search_term,
tables: tableResponse.data.tables,
// users: userResponse.data.users,
}
})).catch((error: AxiosError) => {
// TODO - handle errors
});
}
export function searchExecuteSearch(action) {
const { term, pageIndex } = action;
return axios.get(`/api/search/v0/?query=${term}&page_index=${pageIndex}`)
.then((response: AxiosResponse<SearchResponse>) => transformSearchResults(response.data))
.catch((error: AxiosError) => {
const data = error.response ? error.response.data : {};
return transformSearchResults(data);
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 {
ExecuteSearch,
ExecuteSearchRequest,
ExecuteSearchResponse,
SearchAll,
SearchAllOptions,
SearchAllRequest,
SearchAllResponse,
SearchResource,
SearchResourceRequest,
SearchResourceResponse,
DashboardSearchResults,
TableSearchResults,
UserSearchResults,
} from './types';
import { ResourceType } from "../../components/common/ResourceListItem/types";
export type SearchReducerAction = ExecuteSearchRequest | ExecuteSearchResponse;
export type SearchReducerAction = SearchAllResponse | SearchResourceResponse;
export interface SearchReducerState {
searchTerm: string;
search_term: string;
dashboards: DashboardSearchResults;
tables: TableSearchResults;
users: UserSearchResults;
}
export function executeSearch(term: string, pageIndex: number): ExecuteSearchRequest {
export function searchAll(term: string, options: SearchAllOptions = {}): SearchAllRequest {
return {
options,
term,
type: SearchAll.ACTION,
};
}
export function searchResource(resource: ResourceType, term: string, pageIndex: number): SearchResourceRequest {
return {
pageIndex,
type: ExecuteSearch.ACTION,
term,
resource,
type: SearchResource.ACTION,
};
}
const initialState: SearchReducerState = {
searchTerm: '',
search_term: '',
dashboards: {
page_index: 0,
results: [],
......@@ -44,10 +58,22 @@ const initialState: SearchReducerState = {
};
export default function reducer(state: SearchReducerState = initialState, action: SearchReducerAction): SearchReducerState {
let newState = action.payload;
switch (action.type) {
case ExecuteSearch.SUCCESS:
return action.payload;
case ExecuteSearch.FAILURE:
// SearchAll will reset all resources with search results or the initial state
case SearchAll.SUCCESS:
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;
default:
return state;
......
......@@ -2,24 +2,42 @@ import { call, put, takeEvery } from 'redux-saga/effects';
import { SagaIterator } from 'redux-saga';
import {
ExecuteSearch,
ExecuteSearchRequest,
SearchAll,
SearchAllRequest,
SearchResource,
SearchResourceRequest,
} from './types';
import {
searchExecuteSearch,
searchAll, searchResource,
} from './api/v0';
export function* executeSearchWorker(action: ExecuteSearchRequest): SagaIterator {
// SearchAll
export function* searchAllWorker(action: SearchAllRequest): SagaIterator {
try {
const searchResults = yield call(searchExecuteSearch, action);
yield put({ type: ExecuteSearch.SUCCESS, payload: searchResults });
const searchResults = yield call(searchAll, action);
yield put({ type: SearchAll.SUCCESS, payload: searchResults });
} catch (e) {
yield put({ type: ExecuteSearch.FAILURE });
yield put({ type: SearchAll.FAILURE });
}
}
export function* executeSearchWatcher(): SagaIterator {
yield takeEvery(ExecuteSearch.ACTION, executeSearchWorker);
export function* searchAllWatcher(): SagaIterator {
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';
interface SearchResults<T extends Resource> {
......@@ -14,25 +20,51 @@ export type SearchResponse = {
msg: string;
status_code: number;
search_term: string;
dashboards: DashboardSearchResults;
tables: TableSearchResults;
users: UserSearchResults;
dashboards?: DashboardSearchResults;
tables?: TableSearchResults;
users?: UserSearchResults;
}
/* executeSearch */
export enum ExecuteSearch {
ACTION = 'amundsen/search/EXECUTE_SEARCH',
SUCCESS = 'amundsen/search/EXECUTE_SEARCH_SUCCESS',
FAILURE = 'amundsen/search/EXECUTE_SEARCH_FAILURE',
/* searchAll - Search all resource types */
export enum SearchAll {
ACTION = 'amundsen/search/SEARCH_ALL',
SUCCESS = 'amundsen/search/SEARCH_ALL_SUCCESS',
FAILURE = 'amundsen/search/SEARCH_ALL_FAILURE',
}
export interface ExecuteSearchRequest {
type: ExecuteSearch.ACTION;
export interface SearchAllOptions {
dashboardIndex?: number;
tableIndex?: number;
userIndex?: number;
}
export interface SearchAllRequest {
options: SearchAllOptions;
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;
resource: ResourceType;
term: string;
type: SearchResource.ACTION;
}
export interface ExecuteSearchResponse {
type: ExecuteSearch.SUCCESS | ExecuteSearch.FAILURE;
export interface SearchResourceResponse {
type: SearchResource.SUCCESS | SearchResource.FAILURE;
payload?: SearchReducerState;
}
......@@ -129,10 +129,20 @@ export default function reducer(state: TableMetadataReducerState = initialState,
...state,
isLoading: true,
preview: initialPreviewState,
tableData: initialTableDataState,
tableOwners: tableOwnersReducer(state.tableOwners, action),
tableTags: tableTagsReducer(state.tableTags, action),
};
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:
return {
...state,
......@@ -148,12 +158,13 @@ export default function reducer(state: TableMetadataReducerState = initialState,
case GetColumnDescription.FAILURE:
case GetColumnDescription.SUCCESS:
return { ...state, tableData: action.payload };
case GetLastIndexed.SUCCESS:
return { ...state, lastIndexed: action.payload };
case GetLastIndexed.FAILURE:
return { ...state, lastIndexed: null };
case GetPreviewData.SUCCESS:
case GetLastIndexed.SUCCESS:
return { ...state, lastIndexed: action.payload };
case GetPreviewData.FAILURE:
return { ...state, preview: initialPreviewState };
case GetPreviewData.SUCCESS:
return { ...state, preview: action.payload };
case UpdateTableOwner.ACTION:
case UpdateTableOwner.FAILURE:
......
......@@ -24,10 +24,10 @@ import {
// getTableData
export function* getTableDataWorker(action: GetTableDataRequest): SagaIterator {
try {
const { data, owners, tags } = yield call(metadataGetTableData, action);
yield put({ type: GetTableData.SUCCESS, payload: { data, owners, tags } });
const { data, owners, statusCode, tags } = yield call(metadataGetTableData, action);
yield put({ type: GetTableData.SUCCESS, payload: { data, owners, statusCode, tags } });
} 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 { CurrentUser } from '../types';
import { LoggedInUser, UserResponse } from '../types';
export function getCurrentUser() {
return axios.get(`/api/current_user`)
.then((response: AxiosResponse<CurrentUser>) => {
export function getLoggedInUser() {
return axios.get(`/api/auth_user`)
.then((response: AxiosResponse<LoggedInUser>) => {
return response.data;
}).catch((error: AxiosError) => {
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 {
GetCurrentUser,
GetCurrentUserRequest,
GetCurrentUserResponse,
CurrentUser
GetLoggedInUser,
GetLoggedInUserRequest,
GetLoggedInUserResponse,
GetUser,
GetUserRequest,
GetUserResponse,
LoggedInUser, User
} from './types';
type UserReducerAction = GetCurrentUserRequest | GetCurrentUserResponse;
type UserReducerAction =
GetLoggedInUserRequest | GetLoggedInUserResponse |
GetUserRequest | GetUserResponse ;
export interface UserReducerState {
currentUser: CurrentUser;
loggedInUser: LoggedInUser;
profileUser: User;
}
export function getCurrentUser(): GetCurrentUserRequest {
return { type: GetCurrentUser.ACTION };
export function getLoggedInUser(): GetLoggedInUserRequest {
return { type: GetLoggedInUser.ACTION };
}
export function getUserById(userId: string): GetUserRequest {
return { userId, type: GetUser.ACTION };
}
const defaultUser = {
user_id: '',
display_name: '',
};
const initialState: UserReducerState = {
currentUser: null,
loggedInUser: defaultUser,
profileUser: defaultUser,
};
export default function reducer(state: UserReducerState = initialState, action: UserReducerAction): UserReducerState {
switch (action.type) {
case GetCurrentUser.SUCCESS:
return { ...state, currentUser: action.payload };
case GetLoggedInUser.SUCCESS:
return { ...state, loggedInUser: action.payload };
case GetUser.ACTION:
case GetUser.FAILURE:
return { ...state, profileUser: defaultUser };
case GetUser.SUCCESS:
return { ...state, profileUser: action.payload };
default:
return state;
}
......
import { SagaIterator } from 'redux-saga';
import { call, put, takeEvery } from 'redux-saga/effects';
import { GetCurrentUser } from './types';
import { getCurrentUser } from './api/v0';
import { GetLoggedInUser, GetUser, GetUserRequest } from './types';
import { getLoggedInUser, getUserById } from './api/v0';
export function* getUserWorker(): SagaIterator {
export function* getLoggedInUserWorker(): SagaIterator {
try {
const user = yield call(getCurrentUser);
yield put({ type: GetCurrentUser.SUCCESS, payload: user });
const user = yield call(getLoggedInUser);
const otherUserInfo = yield call(getUserById, user.user_id);
yield put({ type: GetLoggedInUser.SUCCESS, payload: { ...otherUserInfo, ...user }});
} catch (e) {
yield put({ type: GetCurrentUser.FAILURE });
yield put({ type: GetLoggedInUser.FAILURE });
}
}
export function* getCurrentUserWatcher(): SagaIterator {
yield takeEvery(GetCurrentUser.ACTION, getUserWorker);
export function* getLoggedInUserWatcher(): SagaIterator {
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;
email: string;
first_name: string;
last_name: string;
profile_url: string;
email?: string;
first_name?: string;
github_name?: 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',
SUCCESS = 'amundsen/user/GET_SUCCESS',
FAILURE = 'amundsen/user/GET_FAILURE',
}
export interface GetCurrentUserRequest {
type: GetCurrentUser.ACTION;
export interface GetUserRequest {
type: GetUser.ACTION;
userId: string;
}
export interface GetCurrentUserResponse {
type: GetCurrentUser.SUCCESS | GetCurrentUser.FAILURE;
payload?: CurrentUser;
export interface GetUserResponse {
type: GetUser.SUCCESS | GetUser.FAILURE;
payload?: User;
}
......@@ -15,6 +15,7 @@ import Feedback from './components/Feedback';
import Footer from './components/Footer';
import NavBar from './components/NavBar';
import NotFoundPage from './components/NotFoundPage';
import ProfilePage from './components/ProfilePage';
import SearchPage from './components/SearchPage';
import TableDetail from './components/TableDetail';
......@@ -38,6 +39,7 @@ ReactDOM.render(
<Route path="/announcements" component={AnnouncementPage} />
<Route path="/browse" component={BrowsePage} />
<Route path="/search" component={SearchPage} />
<Route path="/user/:userId" component={ProfilePage} />
<Route path="/404" component={NotFoundPage} />
<Route path="/" component={SearchPage} />
</Switch>
......
......@@ -66,7 +66,6 @@ class MetadataTest(unittest.TestCase):
'id': 'test_id',
'description': 'This is a test'
},
'last_updated_timestamp': 1534191754
}
self.expected_parsed_metadata = {
'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