From 0ac742fafffddafe8a5622b5488660ee3fcdfaa9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 11 Apr 2019 11:55:12 -0700 Subject: [PATCH] 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 --- .../amundsen_application/api/metadata/v0.py | 31 +++ .../amundsen_application/api/search/v0.py | 140 +++++++++- frontend/amundsen_application/api/v0.py | 8 +- frontend/amundsen_application/config.py | 2 +- .../amundsen_application/log/action_log.py | 4 +- frontend/amundsen_application/models/user.py | 30 ++- .../static/css/_icons.scss | 20 ++ .../static/css/_list-group-item.scss | 3 +- .../static/css/_variables.scss | 1 - .../static/css/styles.scss | 6 +- .../static/images/icons/github.svg | 1 + .../static/images/icons/mail.svg | 1 + .../static/images/icons/slack.svg | 1 + .../static/images/icons/users.svg | 1 + .../static/js/components/NavBar/index.tsx | 38 +-- .../js/components/NotFoundPage/index.tsx | 4 +- .../js/components/NotFoundPage/styles.scss | 10 +- .../js/components/ProfilePage/index.tsx | 165 ++++++++++++ .../js/components/ProfilePage/styles.scss | 63 +++++ .../static/js/components/ProfilePage/types.ts | 0 .../components/SearchPage/SearchBar/index.tsx | 4 +- .../SearchPage/SearchBar/styles.scss | 6 +- .../static/js/components/SearchPage/index.tsx | 243 +++++++++++------- .../js/components/SearchPage/styles.scss | 101 +++++--- .../TableDetail/DataPreviewButton/index.tsx | 1 - .../DetailList/DetailListItem/styles.scss | 3 +- .../js/components/TableDetail/index.tsx | 8 +- .../js/components/common/Breadcrumb/index.tsx | 29 +++ .../components/common/Breadcrumb/styles.scss | 36 +++ .../js/components/common/Flag/index.tsx | 4 +- .../ResourceListItem/TableListItem/index.tsx | 60 +++-- .../TableListItem/styles.scss | 54 ---- .../ResourceListItem/UserListItem/index.tsx | 57 ++++ .../common/ResourceListItem/index.tsx | 13 +- .../common/ResourceListItem/styles.scss | 51 ++++ .../common/ResourceListItem/types.ts | 54 +++- .../js/components/common/Tabs/index.tsx | 5 +- .../js/components/common/Tabs/styles.scss | 63 +++-- .../static/js/ducks/rootSaga.ts | 10 +- .../static/js/ducks/search/api/v0.ts | 53 ++-- .../static/js/ducks/search/reducer.ts | 48 +++- .../static/js/ducks/search/sagas.ts | 36 ++- .../static/js/ducks/search/types.ts | 58 ++++- .../static/js/ducks/tableMetadata/reducer.ts | 19 +- .../static/js/ducks/tableMetadata/sagas.ts | 6 +- .../static/js/ducks/user/api/v0.ts | 18 +- .../static/js/ducks/user/reducer.ts | 43 +++- .../static/js/ducks/user/sagas.ts | 30 ++- .../static/js/ducks/user/types.ts | 51 +++- .../amundsen_application/static/js/index.tsx | 2 + frontend/tests/unit/api/metadata/test_v0.py | 1 - frontend/tests/unit/api/search/test_v0.py | 152 +++++++++-- 52 files changed, 1427 insertions(+), 421 deletions(-) create mode 100644 frontend/amundsen_application/static/images/icons/github.svg create mode 100644 frontend/amundsen_application/static/images/icons/mail.svg create mode 100644 frontend/amundsen_application/static/images/icons/slack.svg create mode 100644 frontend/amundsen_application/static/images/icons/users.svg create mode 100644 frontend/amundsen_application/static/js/components/ProfilePage/index.tsx create mode 100644 frontend/amundsen_application/static/js/components/ProfilePage/styles.scss create mode 100644 frontend/amundsen_application/static/js/components/ProfilePage/types.ts create mode 100644 frontend/amundsen_application/static/js/components/common/Breadcrumb/index.tsx create mode 100644 frontend/amundsen_application/static/js/components/common/Breadcrumb/styles.scss create mode 100644 frontend/amundsen_application/static/js/components/common/ResourceListItem/UserListItem/index.tsx create mode 100644 frontend/amundsen_application/static/js/components/common/ResourceListItem/styles.scss diff --git a/frontend/amundsen_application/api/metadata/v0.py b/frontend/amundsen_application/api/metadata/v0.py index 2e3b35a32d..9adfea9e29 100644 --- a/frontend/amundsen_application/api/metadata/v0.py +++ b/frontend/amundsen_application/api/metadata/v0.py @@ -553,3 +553,34 @@ def _log_update_table_tags(*, table_key: str, method: str, tag: str) -> None: 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) diff --git a/frontend/amundsen_application/api/search/v0.py b/frontend/amundsen_application/api/search/v0.py index b29c7693b0..2b4644d97b 100644 --- a/frontend/amundsen_application/api/search/v0.py +++ b/frontend/amundsen_application/api/search/v0.py @@ -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), + } + users = { + 'page_index': int(page_index), + 'results': [], + 'total_results': 0, + } -def _search_people(*, search_term: str, page_index: int) -> Dict[str, Any]: - return {} + 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 _map_table_result(result: Dict) -> Dict: '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 _map_table_result(result: Dict) -> Dict: 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 {} diff --git a/frontend/amundsen_application/api/v0.py b/frontend/amundsen_application/api/v0.py index 10cd654e07..22f8f5938e 100644 --- a/frontend/amundsen_application/api/v0.py +++ b/frontend/amundsen_application/api/v0.py @@ -11,11 +11,11 @@ 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() diff --git a/frontend/amundsen_application/config.py b/frontend/amundsen_application/config.py index 694a3245c0..50ea398e2d 100644 --- a/frontend/amundsen_application/config.py +++ b/frontend/amundsen_application/config.py @@ -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 diff --git a/frontend/amundsen_application/log/action_log.py b/frontend/amundsen_application/log/action_log.py index 72f78205bc..19382125b3 100644 --- a/frontend/amundsen_application/log/action_log.py +++ b/frontend/amundsen_application/log/action_log.py @@ -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() diff --git a/frontend/amundsen_application/models/user.py b/frontend/amundsen_application/models/user.py index d1a65c4b6e..8b46a48e60 100644 --- a/frontend/amundsen_application/models/user.py +++ b/frontend/amundsen_application/models/user.py @@ -13,18 +13,36 @@ 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 @@ def make_user(self, data: Dict) -> User: 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: diff --git a/frontend/amundsen_application/static/css/_icons.scss b/frontend/amundsen_application/static/css/_icons.scss index 454b9fbaca..895d943db0 100644 --- a/frontend/amundsen_application/static/css/_icons.scss +++ b/frontend/amundsen_application/static/css/_icons.scss @@ -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, diff --git a/frontend/amundsen_application/static/css/_list-group-item.scss b/frontend/amundsen_application/static/css/_list-group-item.scss index a82f29a95f..5d77f6203b 100644 --- a/frontend/amundsen_application/static/css/_list-group-item.scss +++ b/frontend/amundsen_application/static/css/_list-group-item.scss @@ -6,8 +6,7 @@ border-left: none; border-right: none; cursor: pointer; - padding-left: 4px; - padding-right: 4px; + padding: 0; &:hover { border-color: $gray; diff --git a/frontend/amundsen_application/static/css/_variables.scss b/frontend/amundsen_application/static/css/_variables.scss index 16c9fa9496..6893ec49a9 100644 --- a/frontend/amundsen_application/static/css/_variables.scss +++ b/frontend/amundsen_application/static/css/_variables.scss @@ -4,4 +4,3 @@ @import 'variables-custom'; // Bootstrap Default Values @import '~bootstrap-sass/assets/stylesheets/bootstrap/variables'; - diff --git a/frontend/amundsen_application/static/css/styles.scss b/frontend/amundsen_application/static/css/styles.scss index 31cb603801..06cc931092 100644 --- a/frontend/amundsen_application/static/css/styles.scss +++ b/frontend/amundsen_application/static/css/styles.scss @@ -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; +} diff --git a/frontend/amundsen_application/static/images/icons/github.svg b/frontend/amundsen_application/static/images/icons/github.svg new file mode 100644 index 0000000000..803f8d6795 --- /dev/null +++ b/frontend/amundsen_application/static/images/icons/github.svg @@ -0,0 +1 @@ + diff --git a/frontend/amundsen_application/static/images/icons/mail.svg b/frontend/amundsen_application/static/images/icons/mail.svg new file mode 100644 index 0000000000..2af169e83d --- /dev/null +++ b/frontend/amundsen_application/static/images/icons/mail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/amundsen_application/static/images/icons/slack.svg b/frontend/amundsen_application/static/images/icons/slack.svg new file mode 100644 index 0000000000..5d973466bb --- /dev/null +++ b/frontend/amundsen_application/static/images/icons/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/amundsen_application/static/images/icons/users.svg b/frontend/amundsen_application/static/images/icons/users.svg new file mode 100644 index 0000000000..aacf6b08ec --- /dev/null +++ b/frontend/amundsen_application/static/images/icons/users.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/amundsen_application/static/js/components/NavBar/index.tsx b/frontend/amundsen_application/static/js/components/NavBar/index.tsx index 6d6e9ad963..517c13aeb1 100644 --- a/frontend/amundsen_application/static/js/components/NavBar/index.tsx +++ b/frontend/amundsen_application/static/js/components/NavBar/index.tsx @@ -1,34 +1,31 @@ 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 { @@ -36,17 +33,17 @@ export class NavBar extends React.Component { 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 { }) } { - this.state.currentUser && - + this.state.loggedInUser && + // TODO PEOPLE - Uncomment when enabling people + // + + // } @@ -86,12 +86,12 @@ export class NavBar extends React.Component { 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(mapStateToProps, mapDispatchToProps)(NavBar)); diff --git a/frontend/amundsen_application/static/js/components/NotFoundPage/index.tsx b/frontend/amundsen_application/static/js/components/NotFoundPage/index.tsx index 40771e90f9..ad6a435265 100644 --- a/frontend/amundsen_application/static/js/components/NotFoundPage/index.tsx +++ b/frontend/amundsen_application/static/js/components/NotFoundPage/index.tsx @@ -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 = () => { return ( -
+
+

404 Page Not Found

diff --git a/frontend/amundsen_application/static/js/components/NotFoundPage/styles.scss b/frontend/amundsen_application/static/js/components/NotFoundPage/styles.scss index c6749edd87..8dc20f17f9 100644 --- a/frontend/amundsen_application/static/js/components/NotFoundPage/styles.scss +++ b/frontend/amundsen_application/static/js/components/NotFoundPage/styles.scss @@ -1,8 +1,14 @@ @import 'variables'; .not-found-page { - width: 70%; - text-align: center; + h1 { + text-align: center; + } + + span { + text-align: center; + width: 100%; + } } .glyphicon.glyphicon-exclamation-sign { diff --git a/frontend/amundsen_application/static/js/components/ProfilePage/index.tsx b/frontend/amundsen_application/static/js/components/ProfilePage/index.tsx new file mode 100644 index 0000000000..7dcee7ddba --- /dev/null +++ b/frontend/amundsen_application/static/js/components/ProfilePage/index.tsx @@ -0,0 +1,165 @@ +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 { + 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 ( +
+ { message } +
+ ); + } + + 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 ( + +
+ +
+
+ { + // default Avatar looks a bit jarring -- intentionally not rendering if no display_name + user.display_name && user.display_name.length > 0 && + + } +
+
+
+

{ user.display_name }

+ { + (user.is_active === false) && + + } +
+ { `${user.role_name} on ${user.team_name}` } + { `Manager: ${user.manager_name}` } +
+ { + user.is_active && + + + Slack + + } + { + user.is_active && + + + { user.email } + + } + { + user.is_active && + + + Employee Profile + + } + + + Github + +
+
+
+
+ +
+
+
+ ); + } +} + +const mapStateToProps = (state: GlobalState) => { + return { + user: state.user.profileUser, + } +} + +const mapDispatchToProps = (dispatch) => { + return bindActionCreators({ getUserById }, dispatch); +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ProfilePage); diff --git a/frontend/amundsen_application/static/js/components/ProfilePage/styles.scss b/frontend/amundsen_application/static/js/components/ProfilePage/styles.scss new file mode 100644 index 0000000000..c6d6eee625 --- /dev/null +++ b/frontend/amundsen_application/static/js/components/ProfilePage/styles.scss @@ -0,0 +1,63 @@ +@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; +} diff --git a/frontend/amundsen_application/static/js/components/ProfilePage/types.ts b/frontend/amundsen_application/static/js/components/ProfilePage/types.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/amundsen_application/static/js/components/SearchPage/SearchBar/index.tsx b/frontend/amundsen_application/static/js/components/SearchPage/SearchBar/index.tsx index a841443c31..a36959c0a0 100644 --- a/frontend/amundsen_application/static/js/components/SearchPage/SearchBar/index.tsx +++ b/frontend/amundsen_application/static/js/components/SearchPage/SearchBar/index.tsx @@ -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 { event.preventDefault(); if (this.isFormValid()) { const inputElement = this.inputRef.current; - this.props.handleValueSubmit(inputElement.value.toLowerCase(), 0); + this.props.handleValueSubmit(inputElement.value.toLowerCase()); } }; diff --git a/frontend/amundsen_application/static/js/components/SearchPage/SearchBar/styles.scss b/frontend/amundsen_application/static/js/components/SearchPage/SearchBar/styles.scss index 1205974b5d..4a8a022390 100644 --- a/frontend/amundsen_application/static/js/components/SearchPage/SearchBar/styles.scss +++ b/frontend/amundsen_application/static/js/components/SearchPage/SearchBar/styles.scss @@ -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 { diff --git a/frontend/amundsen_application/static/js/components/SearchPage/index.tsx b/frontend/amundsen_application/static/js/components/SearchPage/index.tsx index abef2aa07e..872153120c 100644 --- a/frontend/amundsen_application/static/js/components/SearchPage/index.tsx +++ b/frontend/amundsen_application/static/js/components/SearchPage/index.tsx @@ -8,13 +8,15 @@ import Pagination from 'react-js-pagination'; import SearchBar from './SearchBar'; import SearchList from './SearchList'; import InfoButton from '../common/InfoButton'; -import { TableResource } from "../common/ResourceListItem/types"; +import { ResourceType, TableResource } from "../common/ResourceListItem/types"; import { GlobalState } from "../../ducks/rootReducer"; -import { executeSearch } from '../../ducks/search/reducer'; +import { searchAll, searchResource } from '../../ducks/search/reducer'; import { - ExecuteSearchRequest, DashboardSearchResults, + SearchAllOptions, + SearchAllRequest, + SearchResourceRequest, TableSearchResults, UserSearchResults } from "../../ducks/search/types"; @@ -23,6 +25,7 @@ import { GetPopularTablesRequest } from '../../ducks/popularTables/types'; // TODO: Use css-modules instead of 'import' import './styles.scss'; +import TabsComponent from "../common/Tabs"; const RESULTS_PER_PAGE = 10; @@ -36,20 +39,21 @@ export interface StateFromProps { } export interface DispatchFromProps { - executeSearch: (term: string, pageIndex: number) => ExecuteSearchRequest; + searchAll: (term: string, options?: SearchAllOptions) => SearchAllRequest; + searchResource: (resource: ResourceType, term: string, pageIndex: number) => SearchResourceRequest; getPopularTables: () => GetPopularTablesRequest; } type SearchPageProps = StateFromProps & DispatchFromProps; interface SearchPageState { - pageIndex: number; - searchTerm: string; + selectedTab: ResourceType; } export class SearchPage extends React.Component { public static defaultProps: SearchPageProps = { - executeSearch: () => undefined, + searchAll: () => undefined, + searchResource: () => undefined, getPopularTables: () => undefined, searchTerm: '', popularTables: [], @@ -73,102 +77,168 @@ export class SearchPage extends React.Component 0) { - const index = pageIndex || '0'; - this.props.executeSearch(searchTerm, index); + const index = pageIndex || 0; + this.props.searchAll(searchTerm, this.getSearchOptions(index, validTab)); + // Update the page URL with validated parameters. + this.updatePageUrl(searchTerm, validTab, index); } } - createErrorMessage() { - const items = this.props.tables; - const { page_index, total_results } = items; - const { searchTerm } = this.props; - if (total_results === 0 && searchTerm.length > 0) { - return ( - - ) + validateTab = (newTab) => { + switch(newTab) { + case ResourceType.table: + case ResourceType.user: + return newTab; + case ResourceType.dashboard: + default: + return this.state.selectedTab; } - if (total_results > 0 && (RESULTS_PER_PAGE * page_index) + 1 > total_results) { - return ( - - ) + }; + + getSearchOptions = (pageIndex, selectedTab) => { + return { + dashboardIndex: (selectedTab === ResourceType.dashboard) ? pageIndex : 0, + userIndex: (selectedTab === ResourceType.user) ? pageIndex : 0, + tableIndex: (selectedTab === ResourceType.table) ? pageIndex : 0, + }; + }; + + getPageIndex = (tab) => { + switch(tab) { + case ResourceType.table: + return this.props.tables.page_index; + case ResourceType.user: + return this.props.users.page_index; + case ResourceType.dashboard: + return this.props.dashboards.page_index; } - return null; - } + return 0; + }; + + onSearchBarSubmit = (searchTerm: string) => { + this.props.searchAll(searchTerm); + this.updatePageUrl(searchTerm, this.state.selectedTab,0); + }; - handlePageChange(pageNumber) { + onPaginationChange = (pageNumber) => { // subtract 1 : pagination component indexes from 1, while our api is 0-indexed - this.updateQueryString(this.props.searchTerm, pageNumber - 1); - } + const index = pageNumber - 1; + + this.props.searchResource(this.state.selectedTab, this.props.searchTerm, index); + this.updatePageUrl(this.props.searchTerm, this.state.selectedTab, index); + }; - updateQueryString(searchTerm, pageIndex) { - const pathName = `/search?searchTerm=${searchTerm}&pageIndex=${pageIndex}`; + onTabChange = (tab: ResourceType) => { + const validTab = this.validateTab(tab); + this.setState({ selectedTab: validTab }); + this.updatePageUrl(this.props.searchTerm, validTab, this.getPageIndex(validTab)); + }; + + updatePageUrl = (searchTerm, tab, pageIndex) => { + const pathName = `/search?searchTerm=${searchTerm}&selectedTab=${tab}&pageIndex=${pageIndex}`; window.history.pushState({}, '', `${window.location.origin}${pathName}`); - this.props.executeSearch(searchTerm, pageIndex); - } + }; - // TODO: Hard-coded text strings should be translatable/customizable - renderSearchResults() { - const errorMessage = this.createErrorMessage(); - if (errorMessage) { - return ( -
+ renderPopularTables = () => { + const searchListParams = { + source: 'popular_tables', + paginationStartIndex: 0, + }; + return ( +
-
- { errorMessage } +
+ +
+
) - } + }; + + renderSearchResults = () => { + const tabConfig = [ + { + title: `Tables (${ this.props.tables.total_results })`, + key: ResourceType.table, + content: this.getTabContent(this.props.tables, 'tables'), + }, + // TODO PEOPLE - Uncomment when enabling people + // { + // title: `Users (${ this.props.users.total_results })`, + // key: ResourceType.user, + // content: this.getTabContent(this.props.users, 'users'), + // }, + ]; + + return ( +
+ +
+ ); + }; + - const items = this.props.tables; - const { page_index, results, total_results } = items; - const { popularTables } = this.props; - - const showResultsList = results.length > 0 || popularTables.length > 0; - - if (showResultsList) { - const startIndex = (RESULTS_PER_PAGE * page_index) + 1; - const endIndex = RESULTS_PER_PAGE * ( page_index + 1); - let listTitle = `${startIndex}-${Math.min(endIndex, total_results)} of ${total_results} results`; - let infoText = "Ordered by the relevance of matches within a resource's metadata, as well as overall usage."; - const searchListParams = { - source: 'search_results', - paginationStartIndex: RESULTS_PER_PAGE * page_index - }; - - const showPopularTables = total_results < 1; - if (showPopularTables) { - listTitle = 'Popular Tables'; - infoText = "These are some of the most commonly accessed tables within your organization."; - searchListParams.source = 'popular_tables'; - } + // TODO: Hard-coded text strings should be translatable/customizable + getTabContent = (results, tabLabel) => { + const { searchTerm } = this.props; + const { page_index, total_results } = results; + const startIndex = (RESULTS_PER_PAGE * page_index) + 1; + const endIndex = RESULTS_PER_PAGE * ( page_index + 1); + + // TODO - Move error messages into Tab Component + // Check no results + if (total_results === 0 && searchTerm.length > 0) { return ( -
-
-
- - -
- +
+
+ Your search - { searchTerm } - did not match any { tabLabel } result +
+
+ ) + } + + // Check page_index bounds + if (page_index < 0 || startIndex > total_results) { + return ( +
+
+ Page index out of bounds for available matches.
-
+
+ ) + } + + let title =`${startIndex}-${Math.min(endIndex, total_results)} of ${total_results} results`; + return ( +
+
+ + +
+ +
{ total_results > RESULTS_PER_PAGE && }
-
- ) - } - } +
+ ); + }; - // TODO: Hard-coded text strings should be translatable/customizable render() { const { searchTerm } = this.props; const innerContent = (
- - { this.renderSearchResults() } + + { searchTerm.length > 0 && this.renderSearchResults() } + { searchTerm.length === 0 && this.renderPopularTables() }
); @@ -209,7 +278,7 @@ export class SearchPage extends React.Component { return { - searchTerm: state.search.searchTerm, + searchTerm: state.search.search_term, popularTables: state.popularTables, tables: state.search.tables, users: state.search.users, @@ -218,7 +287,7 @@ export const mapStateToProps = (state: GlobalState) => { }; export const mapDispatchToProps = (dispatch: any) => { - return bindActionCreators({ executeSearch, getPopularTables } , dispatch); + return bindActionCreators({ searchAll, searchResource, getPopularTables } , dispatch); }; export default connect(mapStateToProps, mapDispatchToProps)(SearchPage); diff --git a/frontend/amundsen_application/static/js/components/SearchPage/styles.scss b/frontend/amundsen_application/static/js/components/SearchPage/styles.scss index 4ec9cdfe22..376d66ee03 100644 --- a/frontend/amundsen_application/static/js/components/SearchPage/styles.scss +++ b/frontend/amundsen_application/static/js/components/SearchPage/styles.scss @@ -1,57 +1,86 @@ @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; + + 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; - font-weight: $font-weight-sans-serif-bold; - font-size: 20px; - margin-top: auto; - margin-bottom: auto; - width: fit-content; - line-height: 24px; + + .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; - 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 { - z-index: 0; - 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 { - z-index: 0; - color: $link-hover-color; - background-color: $gray-lighter; + .pagination > li { + > a, + > span { + border: 1px solid $gray-lighter; + 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; + color: white; + z-index: 0; + } + } + } + } } diff --git a/frontend/amundsen_application/static/js/components/TableDetail/DataPreviewButton/index.tsx b/frontend/amundsen_application/static/js/components/TableDetail/DataPreviewButton/index.tsx index e0887bb381..cd728d18a3 100644 --- a/frontend/amundsen_application/static/js/components/TableDetail/DataPreviewButton/index.tsx +++ b/frontend/amundsen_application/static/js/components/TableDetail/DataPreviewButton/index.tsx @@ -1,6 +1,5 @@ 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' diff --git a/frontend/amundsen_application/static/js/components/TableDetail/DetailList/DetailListItem/styles.scss b/frontend/amundsen_application/static/js/components/TableDetail/DetailList/DetailListItem/styles.scss index b0b46ac65d..33342cbb80 100644 --- a/frontend/amundsen_application/static/js/components/TableDetail/DetailList/DetailListItem/styles.scss +++ b/frontend/amundsen_application/static/js/components/TableDetail/DetailList/DetailListItem/styles.scss @@ -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; diff --git a/frontend/amundsen_application/static/js/components/TableDetail/index.tsx b/frontend/amundsen_application/static/js/components/TableDetail/index.tsx index f149aa6194..5167873452 100644 --- a/frontend/amundsen_application/static/js/components/TableDetail/index.tsx +++ b/frontend/amundsen_application/static/js/components/TableDetail/index.tsx @@ -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 +
+
) } else { innerContent = (
+
{ `${data.schema}.${data.table_name}` }
diff --git a/frontend/amundsen_application/static/js/components/common/Breadcrumb/index.tsx b/frontend/amundsen_application/static/js/components/common/Breadcrumb/index.tsx new file mode 100644 index 0000000000..1329386f1c --- /dev/null +++ b/frontend/amundsen_application/static/js/components/common/Breadcrumb/index.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +import './styles.scss'; + +interface BreadcrumbProps { + path: string; + text: string; +} + +const Breadcrumb: React.SFC = ({ path, text }) => { + return ( +
+ + + +
+ ); +}; + +Breadcrumb.defaultProps = { + path: '/', + text: 'Home', +}; + +export default Breadcrumb; diff --git a/frontend/amundsen_application/static/js/components/common/Breadcrumb/styles.scss b/frontend/amundsen_application/static/js/components/common/Breadcrumb/styles.scss new file mode 100644 index 0000000000..ea9043a9dd --- /dev/null +++ b/frontend/amundsen_application/static/js/components/common/Breadcrumb/styles.scss @@ -0,0 +1,36 @@ +@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; + } +} diff --git a/frontend/amundsen_application/static/js/components/common/Flag/index.tsx b/frontend/amundsen_application/static/js/components/common/Flag/index.tsx index eb5fb09663..f4a84d66e6 100644 --- a/frontend/amundsen_application/static/js/components/common/Flag/index.tsx +++ b/frontend/amundsen_application/static/js/components/common/Flag/index.tsx @@ -32,14 +32,14 @@ const Flag: React.SFC = ({ caseType, text, labelStyle }) => { // TODO: After upgrading to Bootstrap 4, this component should leverage badges // https://getbootstrap.com/docs/4.1/components/badge/ return ( - {convertText(text, caseType)} + {convertText(text, caseType)} ); }; Flag.defaultProps = { caseType: null, text: '', - labelStyle: 'label-default', + labelStyle: 'default', }; export default Flag; diff --git a/frontend/amundsen_application/static/js/components/common/ResourceListItem/TableListItem/index.tsx b/frontend/amundsen_application/static/js/components/common/ResourceListItem/TableListItem/index.tsx index f54b65fe95..a97ecbedec 100644 --- a/frontend/amundsen_application/static/js/components/common/ResourceListItem/TableListItem/index.tsx +++ b/frontend/amundsen_application/static/js/components/common/ResourceListItem/TableListItem/index.tsx @@ -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 { 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 ( - - ) - } - 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 ( -
  • - - -
    - { `${item.schema_name}.${item.name} `} - { item.description } +
  • + + +
    +
    +
    { `${table.schema_name}.${table.name}`}
    +
    { table.description }
    +
    + {/*
    */} + {/*
    Frequent Users
    */} + {/*
    */} + {/**/} + {/*
    */} + {/*
    */} + { + hasLastUpdated && +
    +
    Latest Data
    +
    + { dateLabel } +
    +
    + }
    - { /*createLastUpdatedTimestamp()*/ }
  • diff --git a/frontend/amundsen_application/static/js/components/common/ResourceListItem/TableListItem/styles.scss b/frontend/amundsen_application/static/js/components/common/ResourceListItem/TableListItem/styles.scss index e5b2e698c7..e69de29bb2 100644 --- a/frontend/amundsen_application/static/js/components/common/ResourceListItem/TableListItem/styles.scss +++ b/frontend/amundsen_application/static/js/components/common/ResourceListItem/TableListItem/styles.scss @@ -1,54 +0,0 @@ -@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; - } -} diff --git a/frontend/amundsen_application/static/js/components/common/ResourceListItem/UserListItem/index.tsx b/frontend/amundsen_application/static/js/components/common/ResourceListItem/UserListItem/index.tsx new file mode 100644 index 0000000000..aca046b224 --- /dev/null +++ b/frontend/amundsen_application/static/js/components/common/ResourceListItem/UserListItem/index.tsx @@ -0,0 +1,57 @@ +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 { + 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 ( +
  • + + +
    +
    +
    + { user.name } + { + !user.active && + + } +
    +
    + { `${user.role} on ${user.team_name}` } +
    +
    +
    +
    Frequently Uses
    +
    + { /*TODO Fill this with a real value*/ } + +
    +
    +
    + + +
  • + ); + } +} + +export default UserListItem; diff --git a/frontend/amundsen_application/static/js/components/common/ResourceListItem/index.tsx b/frontend/amundsen_application/static/js/components/common/ResourceListItem/index.tsx index 50fe3cf822..5fa4561694 100644 --- a/frontend/amundsen_application/static/js/components/common/ResourceListItem/index.tsx +++ b/frontend/amundsen_application/static/js/components/common/ResourceListItem/index.tsx @@ -1,7 +1,11 @@ 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 render() { switch(this.props.item.type) { case ResourceType.table: - return (); - // case ListItemType.user: - // case ListItemType.dashboard: + return (); + case ResourceType.user: + return (); + // case ResourceType.dashboard: default: return (null); } diff --git a/frontend/amundsen_application/static/js/components/common/ResourceListItem/styles.scss b/frontend/amundsen_application/static/js/components/common/ResourceListItem/styles.scss new file mode 100644 index 0000000000..b960205efe --- /dev/null +++ b/frontend/amundsen_application/static/js/components/common/ResourceListItem/styles.scss @@ -0,0 +1,51 @@ +@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; + } + } + } + } +} diff --git a/frontend/amundsen_application/static/js/components/common/ResourceListItem/types.ts b/frontend/amundsen_application/static/js/components/common/ResourceListItem/types.ts index 78a5556c0b..cd3e078a00 100644 --- a/frontend/amundsen_application/static/js/components/common/ResourceListItem/types.ts +++ b/frontend/amundsen_application/static/js/components/common/ResourceListItem/types.ts @@ -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. diff --git a/frontend/amundsen_application/static/js/components/common/Tabs/index.tsx b/frontend/amundsen_application/static/js/components/common/Tabs/index.tsx index c4c4254345..af892b92d4 100644 --- a/frontend/amundsen_application/static/js/components/common/Tabs/index.tsx +++ b/frontend/amundsen_application/static/js/components/common/Tabs/index.tsx @@ -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 = ({tabs, defaultTab, onSelect}) => { +const TabsComponent: React.SFC = ({tabs, activeKey, defaultTab, onSelect}) => { return ( { diff --git a/frontend/amundsen_application/static/js/components/common/Tabs/styles.scss b/frontend/amundsen_application/static/js/components/common/Tabs/styles.scss index 95a02abcfb..db8d25a9e8 100644 --- a/frontend/amundsen_application/static/js/components/common/Tabs/styles.scss +++ b/frontend/amundsen_application/static/js/components/common/Tabs/styles.scss @@ -1,43 +1,42 @@ @import 'variables'; -.tabs-component { - .nav.nav-tabs { - border: none; +.tabs-component .nav.nav-tabs { + border: none; - > li { - &.active > a { - &, - &:hover { - color: $brand-color-4; - } + > li { + &.active > a { + &, + &:hover { + color: $brand-color-4; + } - &:after { - opacity: 1; - } + &:after { + opacity: 1; } + } - > a { - background: none; - border: none; - color: $text-medium; - font-size: $font-size-large; - line-height: $line-height-large; + > a { + background: none; + border: none; + color: $text-medium; + font-size: $font-size-large; + line-height: $line-height-large; + padding: 4px 8px 12px; - &:hover { - color: $text-dark; - } + &:hover { + color: $text-dark; + } - // Active tab indicator - &:after { - border: 2px solid $brand-color-4; - bottom: 0; - content: ""; - left: 0; - opacity: 0; - position: absolute; - transition: opacity 200ms ease-in; - width: 100%; - } + // Active tab indicator + &:after { + border: 2px solid $brand-color-4; + bottom: 0; + content: ""; + left: 0; + opacity: 0; + position: absolute; + transition: opacity 200ms ease-in; + width: 100%; } } } diff --git a/frontend/amundsen_application/static/js/ducks/rootSaga.ts b/frontend/amundsen_application/static/js/ducks/rootSaga.ts index b54869a016..0d4e04e6c4 100644 --- a/frontend/amundsen_application/static/js/ducks/rootSaga.ts +++ b/frontend/amundsen_application/static/js/ducks/rootSaga.ts @@ -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(), ]); } diff --git a/frontend/amundsen_application/static/js/ducks/search/api/v0.ts b/frontend/amundsen_application/static/js/ducks/search/api/v0.ts index e627cd6797..f8d3bc8134 100644 --- a/frontend/amundsen_application/static/js/ducks/search/api/v0.ts +++ b/frontend/amundsen_application/static/js/ducks/search/api/v0.ts @@ -1,24 +1,41 @@ -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) { - const { term, pageIndex } = action; - return axios.get(`/api/search/v0/?query=${term}&page_index=${pageIndex}`) - .then((response: AxiosResponse) => transformSearchResults(response.data)) - .catch((error: AxiosError) => { - const data = error.response ? error.response.data : {}; - return transformSearchResults(data); +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 /*, userResponse: AxiosResponse*/) => { + 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 + }); +} diff --git a/frontend/amundsen_application/static/js/ducks/search/reducer.ts b/frontend/amundsen_application/static/js/ducks/search/reducer.ts index 415aab8559..a4167f25fc 100644 --- a/frontend/amundsen_application/static/js/ducks/search/reducer.ts +++ b/frontend/amundsen_application/static/js/ducks/search/reducer.ts @@ -1,31 +1,45 @@ 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; diff --git a/frontend/amundsen_application/static/js/ducks/search/sagas.ts b/frontend/amundsen_application/static/js/ducks/search/sagas.ts index dd1f2136c2..638db7096c 100644 --- a/frontend/amundsen_application/static/js/ducks/search/sagas.ts +++ b/frontend/amundsen_application/static/js/ducks/search/sagas.ts @@ -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); } diff --git a/frontend/amundsen_application/static/js/ducks/search/types.ts b/frontend/amundsen_application/static/js/ducks/search/types.ts index 2159487c31..98a0b99fca 100644 --- a/frontend/amundsen_application/static/js/ducks/search/types.ts +++ b/frontend/amundsen_application/static/js/ducks/search/types.ts @@ -1,4 +1,10 @@ -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 { @@ -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; } diff --git a/frontend/amundsen_application/static/js/ducks/tableMetadata/reducer.ts b/frontend/amundsen_application/static/js/ducks/tableMetadata/reducer.ts index b5dfe7b76b..cf611bd945 100644 --- a/frontend/amundsen_application/static/js/ducks/tableMetadata/reducer.ts +++ b/frontend/amundsen_application/static/js/ducks/tableMetadata/reducer.ts @@ -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: + return { ...state, lastIndexed: null }; + 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: diff --git a/frontend/amundsen_application/static/js/ducks/tableMetadata/sagas.ts b/frontend/amundsen_application/static/js/ducks/tableMetadata/sagas.ts index 4e56326c16..402b794b57 100644 --- a/frontend/amundsen_application/static/js/ducks/tableMetadata/sagas.ts +++ b/frontend/amundsen_application/static/js/ducks/tableMetadata/sagas.ts @@ -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: [] } }); } } diff --git a/frontend/amundsen_application/static/js/ducks/user/api/v0.ts b/frontend/amundsen_application/static/js/ducks/user/api/v0.ts index 527b543a71..57dc28fd34 100644 --- a/frontend/amundsen_application/static/js/ducks/user/api/v0.ts +++ b/frontend/amundsen_application/static/js/ducks/user/api/v0.ts @@ -1,12 +1,22 @@ 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) => { +export function getLoggedInUser() { + return axios.get(`/api/auth_user`) + .then((response: AxiosResponse) => { 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) => { + return response.data.user; + }) + .catch((error: AxiosError) => { + return {}; + }); +} diff --git a/frontend/amundsen_application/static/js/ducks/user/reducer.ts b/frontend/amundsen_application/static/js/ducks/user/reducer.ts index bf6b05167e..d2cc03fb22 100644 --- a/frontend/amundsen_application/static/js/ducks/user/reducer.ts +++ b/frontend/amundsen_application/static/js/ducks/user/reducer.ts @@ -1,28 +1,49 @@ 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; } diff --git a/frontend/amundsen_application/static/js/ducks/user/sagas.ts b/frontend/amundsen_application/static/js/ducks/user/sagas.ts index 9463fb5e9f..13b39a028c 100644 --- a/frontend/amundsen_application/static/js/ducks/user/sagas.ts +++ b/frontend/amundsen_application/static/js/ducks/user/sagas.ts @@ -1,18 +1,32 @@ 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); } diff --git a/frontend/amundsen_application/static/js/ducks/user/types.ts b/frontend/amundsen_application/static/js/ducks/user/types.ts index b63d4702c8..e28c9d1331 100644 --- a/frontend/amundsen_application/static/js/ducks/user/types.ts +++ b/frontend/amundsen_application/static/js/ducks/user/types.ts @@ -1,22 +1,51 @@ -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; } diff --git a/frontend/amundsen_application/static/js/index.tsx b/frontend/amundsen_application/static/js/index.tsx index 8d35dd9a8b..a62a2725ef 100644 --- a/frontend/amundsen_application/static/js/index.tsx +++ b/frontend/amundsen_application/static/js/index.tsx @@ -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( + diff --git a/frontend/tests/unit/api/metadata/test_v0.py b/frontend/tests/unit/api/metadata/test_v0.py index 4c6d0411d0..b67ab2922e 100644 --- a/frontend/tests/unit/api/metadata/test_v0.py +++ b/frontend/tests/unit/api/metadata/test_v0.py @@ -66,7 +66,6 @@ def setUp(self) -> None: 'id': 'test_id', 'description': 'This is a test' }, - 'last_updated_timestamp': 1534191754 } self.expected_parsed_metadata = { 'cluster': 'test_cluster', diff --git a/frontend/tests/unit/api/search/test_v0.py b/frontend/tests/unit/api/search/test_v0.py index 65711c57a2..445c60609f 100644 --- a/frontend/tests/unit/api/search/test_v0.py +++ b/frontend/tests/unit/api/search/test_v0.py @@ -12,7 +12,7 @@ class SearchTest(unittest.TestCase): def setUp(self) -> None: - self.mock_search_results = { + self.mock_search_table_results = { 'total_results': 1, 'results': [ { @@ -25,83 +25,129 @@ def setUp(self) -> None: 'database': 'test_db', 'description': 'This is a test', 'key': 'test_key', - 'last_updated': 1527283287, + 'last_updated_epoch': 1527283287, 'name': 'test_table', 'schema_name': 'test_schema', 'tags': [], } ] } - self.expected_parsed_search_results = [ + self.expected_parsed_search_table_results = [ { 'type': 'table', 'cluster': 'test_cluster', 'database': 'test_db', 'description': 'This is a test', 'key': 'test_key', - 'last_updated': 1527283287, + 'last_updated_epoch': 1527283287, 'name': 'test_table', 'schema_name': 'test_schema', } ] + self.mock_search_user_results = { + 'total_results': 1, + # TODO update data schema + 'results': [ + { + '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', + } + ] + } + self.expected_parsed_search_user_results = [ + { + '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', + } + ] self.bad_search_results = { 'total_results': 1, 'results': 'Bad results to trigger exception' } - def test_search_fail_if_no_query(self) -> None: + # ----- Table Search Tests ---- # + + def test_search_table_fail_if_no_query(self) -> None: """ Test request failure if 'query' is not provided in the query string to the search endpoint :return: """ with local_app.test_client() as test: - response = test.get('/api/search/v0/', query_string=dict(page_index='0')) + response = test.get('/api/search/v0/table', query_string=dict(page_index='0')) self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) - def test_search_fail_if_no_page_index(self) -> None: + def test_search_table_fail_if_no_page_index(self) -> None: """ Test request failure if 'page_index' is not provided in the query string to the search endpoint :return: """ with local_app.test_client() as test: - response = test.get('/api/search/v0/', query_string=dict(query='test')) + response = test.get('/api/search/v0/table', query_string=dict(query='test')) self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) @responses.activate - def test_search_success(self) -> None: + def test_search_table_success(self) -> None: """ Test request success :return: """ responses.add(responses.GET, local_app.config['SEARCHSERVICE_ENDPOINT'], - json=self.mock_search_results, status=HTTPStatus.OK) + json=self.mock_search_table_results, status=HTTPStatus.OK) with local_app.test_client() as test: - response = test.get('/api/search/v0/', query_string=dict(query='test', page_index='0')) + response = test.get('/api/search/v0/table', query_string=dict(query='test', page_index='0')) data = json.loads(response.data) self.assertEqual(response.status_code, HTTPStatus.OK) tables = data.get('tables') - self.assertEqual(tables.get('total_results'), self.mock_search_results.get('total_results')) - self.assertCountEqual(tables.get('results'), self.expected_parsed_search_results) + self.assertEqual(tables.get('total_results'), self.mock_search_table_results.get('total_results')) + self.assertCountEqual(tables.get('results'), self.expected_parsed_search_table_results) @responses.activate - def test_search_fail_on_non_200_response(self) -> None: + def test_search_table_fail_on_non_200_response(self) -> None: """ Test request failure if search endpoint returns non-200 http code :return: """ responses.add(responses.GET, local_app.config['SEARCHSERVICE_ENDPOINT'], - json=self.mock_search_results, status=HTTPStatus.INTERNAL_SERVER_ERROR) + json=self.mock_search_table_results, status=HTTPStatus.INTERNAL_SERVER_ERROR) with local_app.test_client() as test: - response = test.get('/api/search/v0/', query_string=dict(query='test', page_index='0')) + response = test.get('/api/search/v0/table', query_string=dict(query='test', page_index='0')) self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) @responses.activate - def test_search_fail_on_proccessing_bad_response(self) -> None: + def test_search_table_fail_on_proccessing_bad_response(self) -> None: """ Test catching exception if there is an error processing the results from the search endpoint @@ -111,11 +157,11 @@ def test_search_fail_on_proccessing_bad_response(self) -> None: json=self.bad_search_results, status=HTTPStatus.OK) with local_app.test_client() as test: - response = test.get('/api/search/v0/', query_string=dict(query='test', page_index='0')) + response = test.get('/api/search/v0/table', query_string=dict(query='test', page_index='0')) self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) @responses.activate - def test_search_with_field(self) -> None: + def test_search_table_with_field(self) -> None: """ Test search request if user search with colon :return: @@ -154,3 +200,71 @@ def test_create_url_with_field(self) -> None: '/hive?page_index=1' self.assertEqual(_create_url_with_field(search_term=search_term, page_index=1), expected) + + # ----- User Search Tests ---- # + + def test_search_user_fail_if_no_query(self) -> None: + """ + Test request failure if 'query' is not provided in the query string + to the search endpoint + :return: + """ + with local_app.test_client() as test: + response = test.get('/api/search/v0/user', query_string=dict(page_index='0')) + self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) + + def test_search_user_fail_if_no_page_index(self) -> None: + """ + Test request failure if 'page_index' is not provided in the query string + to the search endpoint + :return: + """ + with local_app.test_client() as test: + response = test.get('/api/search/v0/user', query_string=dict(query='test')) + self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) + + # TODO - Uncomment test once the API is integrated + # @responses.activate + # def test_search_user_success(self) -> None: + # """ + # Test request success + # :return: + # """ + # responses.add(responses.GET, local_app.config['SEARCHSERVICE_ENDPOINT'], + # json=self.mock_search_table_results, status=HTTPStatus.OK) + # + # with local_app.test_client() as test: + # response = test.get('/api/search/v0/user', query_string=dict(query='test', page_index='0')) + # data = json.loads(response.data) + # self.assertEqual(response.status_code, HTTPStatus.OK) + # + # users = data.get('users') + # self.assertEqual(users.get('total_results'), self.mock_search_table_results.get('total_results')) + # self.assertCountEqual(users.get('results'), self.expected_parsed_search_table_results) + + # @responses.activate + # def test_search_user_fail_on_non_200_response(self) -> None: + # """ + # Test request failure if search endpoint returns non-200 http code + # :return: + # """ + # responses.add(responses.GET, local_app.config['SEARCHSERVICE_ENDPOINT'], + # json=self.mock_search_table_results, status=HTTPStatus.INTERNAL_SERVER_ERROR) + # + # with local_app.test_client() as test: + # response = test.get('/api/search/v0/user', query_string=dict(query='test', page_index='0')) + # self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) + + @responses.activate + def test_search_user_fail_on_proccessing_bad_response(self) -> None: + """ + Test catching exception if there is an error processing the results + from the search endpoint + :return: + """ + responses.add(responses.GET, local_app.config['SEARCHSERVICE_ENDPOINT'], + json=self.bad_search_results, status=HTTPStatus.OK) + + with local_app.test_client() as test: + response = test.get('/api/search/v0/table', query_string=dict(query='test', page_index='0')) + self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)