Unverified Commit 63306c91 authored by Daniel's avatar Daniel Committed by GitHub

Add owned/frequently uses/bookmarks to profile page (#196)

* Implement APIs/Saga/Reducer for user 'own' and user 'read' resources
* Added bookmarks, read, own to profile page
* Refactor styles related to pagination and list items
parent a5368433
......@@ -490,3 +490,51 @@ def update_bookmark() -> Response:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
return make_response(jsonify({'msg': message}), HTTPStatus.INTERNAL_SERVER_ERROR)
@metadata_blueprint.route('/user/read', methods=['GET'])
def get_user_read() -> Response:
"""
Calls metadata service to GET read/frequently used resources
:return: a JSON object with an array of read resources
"""
try:
user_id = get_query_param(request.args, 'user_id')
url = '{0}{1}/{2}/read/'.format(app.config['METADATASERVICE_BASE'],
USER_ENDPOINT,
user_id)
response = request_metadata(url=url, method=request.method)
status_code = response.status_code
read_tables_raw = response.json().get('table')
read_tables = [marshall_table_partial(table) for table in read_tables_raw]
return make_response(jsonify({'msg': 'success', 'read': read_tables}), status_code)
except Exception as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
return make_response(jsonify({'msg': message}), HTTPStatus.INTERNAL_SERVER_ERROR)
@metadata_blueprint.route('/user/own', methods=['GET'])
def get_user_own() -> Response:
"""
Calls metadata service to GET owned resources
:return: a JSON object with an array of owned resources
"""
try:
user_id = get_query_param(request.args, 'user_id')
url = '{0}{1}/{2}/own/'.format(app.config['METADATASERVICE_BASE'],
USER_ENDPOINT,
user_id)
response = request_metadata(url=url, method=request.method)
status_code = response.status_code
owned_tables_raw = response.json().get('table')
owned_tables = [marshall_table_partial(table) for table in owned_tables_raw]
return make_response(jsonify({'msg': 'success', 'own': owned_tables}), status_code)
except Exception as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
return make_response(jsonify({'msg': message}), HTTPStatus.INTERNAL_SERVER_ERROR)
// TODO - Try to implement this with bootstrap variables
@import 'variables';
.list-group-item {
border-left: none;
border-right: none;
cursor: pointer;
padding: 0;
&:hover {
border-color: $gray;
background-color: $gray-lightest;
}
&:hover + li {
border-top-color: $gray;
}
}
// TODO - Try to implement this with bootstrap variables
@import 'variables';
.list-group {
margin: 24px 0;
.list-group-item {
border-left: none;
border-right: none;
cursor: pointer;
padding: 0;
&:hover {
border-color: $gray;
background-color: $gray-lightest;
}
&:hover + li {
border-top-color: $gray;
}
}
}
@import 'variables';
.pagination {
display: flex;
justify-content: center;
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;
}
}
}
}
}
......@@ -4,7 +4,8 @@
@import 'buttons';
@import 'fonts';
@import 'icons';
@import 'list-group-item';
@import 'list-group';
@import 'pagination';
@import 'popovers';
@import 'typography';
......
export const BOOKMARKED_LABEL = 'bookmarked';
export const BOOKMARKED_SOURCE = 'profile_bookmark';
export const BOOKMARKED_TAB_KEY = 'bookmark_tab';
export const BOOKMARKED_TAB_TITLE = 'Bookmarked';
export const ITEMS_PER_PAGE = 6;
export const OWNED_LABEL = 'owned';
export const OWNED_SOURCE = 'profile_own';
export const OWNED_TAB_KEY = 'own_tab';
export const OWNED_TAB_TITLE = 'Owned';
export const READ_LABEL = 'frequently used';
export const READ_SOURCE = 'profile_read';
export const READ_TAB_KEY = 'read_tab';
export const READ_TAB_TITLE = 'Frequently Used';
......@@ -9,19 +9,41 @@ import Flag from 'components/common/Flag';
import Tabs from 'components/common/Tabs';
import { GlobalState } from 'ducks/rootReducer';
import { getUserById } from 'ducks/user/reducer';
import { GetUserRequest } from 'ducks/user/types';
import { PeopleUser } from 'interfaces';
import { getUserById, getUserOwn, getUserRead } from 'ducks/user/reducer';
import { PeopleUser, Resource } from 'interfaces';
import { GetUserRequest, GetUserOwnRequest, GetUserReadRequest } from 'ducks/user/types';
import './styles.scss';
import ResourceList from 'components/common/ResourceList';
import { GetBookmarksForUserRequest } from 'ducks/bookmark/types';
import { getBookmarksForUser } from 'ducks/bookmark/reducer';
import {
BOOKMARKED_LABEL,
BOOKMARKED_SOURCE,
BOOKMARKED_TAB_KEY,
BOOKMARKED_TAB_TITLE,
ITEMS_PER_PAGE, OWNED_LABEL,
OWNED_SOURCE, OWNED_TAB_KEY,
OWNED_TAB_TITLE, READ_LABEL,
READ_SOURCE, READ_TAB_KEY,
READ_TAB_TITLE,
} from './constants';
interface StateFromProps {
bookmarks: Resource[];
user: PeopleUser;
own: Resource[];
read: Resource[];
}
interface DispatchFromProps {
getUserById: (userId: string) => GetUserRequest;
getUserOwn: (userId: string) => GetUserOwnRequest;
getUserRead: (userId: string) => GetUserReadRequest;
getBookmarksForUser: (userId: string) => GetBookmarksForUserRequest;
}
export type ProfilePageProps = StateFromProps & DispatchFromProps;
......@@ -39,41 +61,52 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
componentDidMount() {
this.props.getUserById(this.userId);
this.props.getUserOwn(this.userId);
this.props.getUserRead(this.userId);
this.props.getBookmarksForUser(this.userId);
}
getUserId = () => {
return this.userId;
};
// TODO: consider moving logic for empty content into Tab component
createEmptyTabMessage = (message: string) => {
getTabContent = (resource: Resource[], source: string, label: string) => {
// TODO: consider moving logic for empty content into Tab component
if (resource.length === 0) {
return (
<div className="empty-tab-message">
<label>User has no { label } resources.</label>
</div>
);
}
return (
<div className="empty-tab-message">
<label>{ message }</label>
</div>
);
<ResourceList
allItems={ resource }
source={ source }
itemsPerPage={ ITEMS_PER_PAGE }
/>
)
};
generateTabInfo = () => {
const user = this.props.user;
const tabInfo = [];
const { bookmarks, read, own } = this.props;
// 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)',
content: this.getTabContent(read, READ_SOURCE, READ_LABEL),
key: READ_TAB_KEY,
title: `${READ_TAB_TITLE} (${read.length})`,
});
tabInfo.push({
content: this.createEmptyTabMessage('User has no bookmarked resources.'),
key: 'bookmarks_tab',
title: 'Bookmarks (0)',
content: this.getTabContent(bookmarks, BOOKMARKED_SOURCE, BOOKMARKED_LABEL),
key: BOOKMARKED_TAB_KEY,
title: `${BOOKMARKED_TAB_TITLE} (${bookmarks.length})`,
});
tabInfo.push({
content: this.createEmptyTabMessage('User has no owned resources.'),
key: 'owner_tab',
title: 'Owner (0)',
content: this.getTabContent(own, OWNED_SOURCE, OWNED_LABEL),
key: OWNED_TAB_KEY,
title: `${OWNED_TAB_TITLE} (${own.length})`,
});
return tabInfo;
......@@ -90,6 +123,7 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
<div className="col-xs-12 col-md-offset-1 col-md-10">
{/* remove hardcode to home when this page is ready for production */}
<Breadcrumb path="/" text="Home" />
{/* TODO - Consider making this part a separate component */}
<div className="profile-header">
<div id="profile-avatar" className="profile-avatar">
{
......@@ -147,7 +181,7 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
</div>
</div>
<div id="profile-tabs" className="profile-tabs">
<Tabs tabs={ this.generateTabInfo() } defaultTab='frequentUses_tab' />
<Tabs tabs={ this.generateTabInfo() } defaultTab={ READ_TAB_KEY } />
</div>
</div>
</div>
......@@ -159,12 +193,15 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
export const mapStateToProps = (state: GlobalState) => {
return {
user: state.user.profileUser,
user: state.user.profile.user,
own: state.user.profile.own,
read: state.user.profile.read,
bookmarks: state.bookmarks.bookmarksForUser,
}
};
export const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ getUserById }, dispatch);
return bindActionCreators({ getUserById, getUserOwn, getUserRead, getBookmarksForUser }, dispatch);
};
export default connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(ProfilePage);
......@@ -7,15 +7,36 @@ import { shallow } from 'enzyme';
import Breadcrumb from 'components/common/Breadcrumb';
import Flag from 'components/common/Flag';
import Tabs from 'components/common/Tabs';
import { ProfilePage, ProfilePageProps, mapDispatchToProps, mapStateToProps } from '../';
import { mapDispatchToProps, mapStateToProps, ProfilePage, ProfilePageProps } from '../';
import globalState from 'fixtures/globalState';
import { ResourceType } from 'interfaces/Resources';
import {
BOOKMARKED_LABEL,
BOOKMARKED_SOURCE,
BOOKMARKED_TAB_KEY,
OWNED_LABEL, OWNED_SOURCE,
OWNED_TAB_KEY, READ_LABEL,
READ_SOURCE, READ_TAB_KEY,
} from '../constants';
describe('ProfilePage', () => {
const setup = (propOverrides?: Partial<ProfilePageProps>) => {
const props: ProfilePageProps = {
user: globalState.user.profileUser,
user: globalState.user.profile.user,
bookmarks: [
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
],
read: [],
own: [],
getUserById: jest.fn(),
getUserOwn: jest.fn(),
getUserRead: jest.fn(),
getBookmarksForUser: jest.fn(),
...propOverrides
};
// @ts-ignore : complains about match
......@@ -48,35 +69,83 @@ describe('ProfilePage', () => {
const { props, wrapper } = setup();
expect(props.getUserById).toHaveBeenCalled();
});
it('calls props.getUserOwn', () => {
const { props, wrapper } = setup();
expect(props.getUserOwn).toHaveBeenCalled();
});
it('calls props.getUserRead', () => {
const { props, wrapper } = setup();
expect(props.getUserRead).toHaveBeenCalled();
});
it('calls props.getBookmarksForUser', () => {
const { props, wrapper } = setup();
expect(props.getBookmarksForUser).toHaveBeenCalled();
});
});
describe('createEmptyTabMessage', () => {
let content;
describe('getTabContent', () => {
let props;
let wrapper;
beforeAll(() => {
const { props, wrapper } = setup();
content = wrapper.instance().createEmptyTabMessage('Empty message');
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('creates div w/ correct class', () => {
expect(shallow(content).find('div').props().className).toEqual('empty-tab-message');
it('returns an empty tab message when there are no items to render', () => {
const content = wrapper.instance().getTabContent([], 'source', 'label');
expect(shallow(content).find('.empty-tab-message').exists()).toBe(true)
});
it('creates text with given message', () => {
expect(shallow(content).find('label').text()).toEqual('Empty message');
it('renders a ResourceList with the correct props', () => {
const content = wrapper.instance().getTabContent(props.bookmarks, 'source', 'label');
// 'getTabContent' returns a <ResourceList> which shallow will actually render.
// The intent here is not to test the functionality of <ResourceList>
expect(content.props.allItems).toEqual(props.bookmarks);
expect(content.props.source).toEqual('source');
});
});
/* TODO: Implement proper test when the real logic for this component is written */
describe('generateTabInfo', () => {
let tabInfoArray;
let props;
let wrapper;
let getTabContentSpy;
beforeAll(() => {
const { props, wrapper } = setup();
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent');
tabInfoArray = wrapper.instance().generateTabInfo();
});
it('has a passing test so that it does not throw an error', () => {
expect(true).toEqual(true);
it('returns a tab info array with 3 tabs', () => {
expect(tabInfoArray.length).toEqual(3);
});
it('tabInfo contains a tab for frequently used resources', () => {
expect(tabInfoArray.find(tab => tab.key === READ_TAB_KEY)).toBeDefined()
});
it('tabInfo contains a tab for bookmarked resources', () => {
expect(tabInfoArray.find(tab => tab.key === BOOKMARKED_TAB_KEY)).toBeDefined()
});
it('tabInfo contains a tab for owned resources', () => {
expect(tabInfoArray.find(tab => tab.key === OWNED_TAB_KEY)).toBeDefined()
});
it ('calls getTabContent for each of 3 tabs', () => {
expect(getTabContentSpy).toHaveBeenCalledTimes(3);
expect(getTabContentSpy).toHaveBeenCalledWith(props.own, OWNED_SOURCE, OWNED_LABEL);
expect(getTabContentSpy).toHaveBeenCalledWith(props.read, READ_SOURCE, READ_LABEL);
expect(getTabContentSpy).toHaveBeenCalledWith(props.bookmarks, BOOKMARKED_SOURCE, BOOKMARKED_LABEL);
});
});
describe('render', () => {
......@@ -110,7 +179,7 @@ describe('ProfilePage', () => {
it('does not render Avatar if user.display_name is empty string', () => {
const userCopy = {
...globalState.user.profileUser,
...globalState.user.profile.user,
display_name: "",
} ;
const wrapper = setup({
......@@ -125,7 +194,7 @@ describe('ProfilePage', () => {
it('renders Flag with correct props if user not active', () => {
const userCopy = {
...globalState.user.profileUser,
...globalState.user.profile.user,
is_active: false,
};
const wrapper = setup({
......@@ -161,7 +230,7 @@ describe('ProfilePage', () => {
it('renders Tabs w/ correct props', () => {
expect(wrapper.find('#profile-tabs').find(Tabs).props()).toMatchObject({
tabs: wrapper.instance().generateTabInfo(),
defaultTab: 'frequentUses_tab',
defaultTab: READ_TAB_KEY,
});
});
......@@ -205,6 +274,18 @@ describe('mapDispatchToProps', () => {
it('sets getUserById on the props', () => {
expect(result.getUserById).toBeInstanceOf(Function);
});
it('sets getUserOwn on the props', () => {
expect(result.getUserOwn).toBeInstanceOf(Function);
});
it('sets getUserRead on the props', () => {
expect(result.getUserRead).toBeInstanceOf(Function);
});
it('sets getBookmarksForUser on the props', () => {
expect(result.getBookmarksForUser).toBeInstanceOf(Function);
});
});
describe('mapStateToProps', () => {
......@@ -214,6 +295,18 @@ describe('mapStateToProps', () => {
});
it('sets user on the props', () => {
expect(result.user).toEqual(globalState.user.profileUser);
expect(result.user).toEqual(globalState.user.profile.user);
});
it('sets bookmarks on the props', () => {
expect(result.bookmarks).toEqual(globalState.bookmarks.bookmarksForUser);
});
it('sets own on the props', () => {
expect(result.own).toEqual(globalState.user.profile.own);
});
it('sets read on the props', () => {
expect(result.read).toEqual(globalState.user.profile.read);
});
});
@import 'variables';
.search-page {
.tabs-component,
.search-list-container {
margin-top: 32px;
......@@ -23,48 +22,9 @@
font-size: 18px;
}
}
.list-group-item:first-child {
border-top: none;
}
}
.search-error {
text-align: center;
}
.search-pagination-component {
display: flex;
justify-content: center;
}
.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;
}
}
}
}
}
@import 'variables';
.detail-list-item {
.list-group .list-group-item.detail-list-item {
text-decoration: none;
display: flex;
flex-direction: column;
......
......@@ -74,15 +74,13 @@ class ResourceList extends React.Component<ResourceListProps, ResourceListState>
{
paginate &&
itemsCount > itemsPerPage &&
<div className="text-center">
<Pagination
activePage={ activePage + 1 }
itemsCountPerPage={ itemsPerPage }
totalItemsCount={ itemsCount }
pageRangeDisplayed={ PAGINATION_PAGE_RANGE }
onChange={ this.onPagination }
/>
</div>
<Pagination
activePage={ activePage + 1 }
itemsCountPerPage={ itemsPerPage }
totalItemsCount={ itemsCount }
pageRangeDisplayed={ PAGINATION_PAGE_RANGE }
onChange={ this.onPagination }
/>
}
</>
);
......
......@@ -27,3 +27,10 @@
}
}
}
.tab-content {
.list-group-item:first-child {
border-top: none;
}
}
import axios, { AxiosResponse } from 'axios';
export const API_PATH = '/api/metadata/v0';
// TODO - Consider moving 'Bookmarks' under 'User'
// TODO: Define types for the AxiosResponse data
export function addBookmark(resourceKey: string, resourceType: string) {
......@@ -9,18 +9,18 @@ export function addBookmark(resourceKey: string, resourceType: string) {
.then((response: AxiosResponse) => {
return response.data;
});
};
}
export function removeBookmark(resourceKey: string, resourceType: string) {
return axios.delete(`${API_PATH}/user/bookmark`, { data: { type: resourceType, key: resourceKey }})
.then((response: AxiosResponse) => {
return response.data;
});
};
}
export function getBookmarks(userId?: string) {
return axios.get(`${API_PATH}/user/bookmark` + (userId ? `?user_id=${userId}` : ''))
.then((response: AxiosResponse) => {
return response.data;
});
};
}
......@@ -8,6 +8,7 @@ import {
GetBookmarksResponse,
GetBookmarksForUser,
GetBookmarksForUserRequest,
GetBookmarksForUserResponse,
RemoveBookmark,
RemoveBookmarkRequest,
RemoveBookmarkResponse,
......@@ -20,25 +21,25 @@ export function addBookmark(resourceKey: string, resourceType: string): AddBookm
resourceType,
type: AddBookmark.REQUEST,
}
};
}
export function removeBookmark(resourceKey: string, resourceType: string): RemoveBookmarkRequest {
return {
resourceKey,
resourceType,
type: RemoveBookmark.REQUEST,
}
};
}
export function getBookmarks(): GetBookmarksRequest {
return {
type: GetBookmarks.REQUEST,
}
};
}
export function getBookmarksForUser(userId: string): GetBookmarksForUserRequest {
return {
userId,
type: GetBookmarksForUser.REQUEST,
}
};
}
/* REDUCER */
export interface BookmarkReducerState {
......@@ -53,7 +54,7 @@ export const initialState: BookmarkReducerState = {
bookmarksForUser: [],
};
export default function reducer(state: BookmarkReducerState = initialState, action): BookmarkReducerState {
export default function reducer(state: BookmarkReducerState = initialState, action): BookmarkReducerState {
switch(action.type) {
case RemoveBookmark.SUCCESS:
const { resourceKey } = (<RemoveBookmarkResponse>action).payload;
......@@ -68,12 +69,19 @@ export const initialState: BookmarkReducerState = {
myBookmarks: (<GetBookmarksResponse>action).payload.bookmarks,
myBookmarksIsLoaded: true,
};
case AddBookmark.FAILURE:
case GetBookmarks.FAILURE:
case GetBookmarksForUser.SUCCESS:
case GetBookmarksForUser.FAILURE:
return {
...state,
bookmarksForUser: (<GetBookmarksForUserResponse>action).payload.bookmarks,
};
case AddBookmark.FAILURE:
case GetBookmarks.FAILURE:
case RemoveBookmark.FAILURE:
default:
return state;
}
}
import { SagaIterator } from 'redux-saga';
import { call, put, takeEvery } from 'redux-saga/effects';
import {
import {
AddBookmark,
AddBookmarkRequest,
GetBookmarks,
......@@ -30,7 +30,7 @@ export function* addBookmarkWorker(action: AddBookmarkRequest): SagaIterator {
yield put({ type: AddBookmark.SUCCESS, payload: { bookmarks: response.bookmarks } });
} catch(e) {
yield put({ type: AddBookmark.FAILURE, payload: { bookmarks: [] } });
}
}
}
export function* addBookmarkWatcher(): SagaIterator {
yield takeEvery(AddBookmark.REQUEST , addBookmarkWorker)
......@@ -69,14 +69,12 @@ export function* getBookmarksWatcher(): SagaIterator {
export function* getBookmarkForUserWorker(action: GetBookmarksForUserRequest): SagaIterator {
let response;
const { userId } = action;
try {
response = yield call(getBookmarks, userId);
yield put({ type: GetBookmarksForUser.SUCCESS, payload: { userId, bookmarks: response.bookmarks } });
} catch(e) {
yield put({ type: GetBookmarksForUser.FAILURE, payload: { userId, bookmarks: [] } });
}
}
}
export function* getBookmarksForUserWatcher(): SagaIterator {
yield takeEvery(GetBookmarksForUser.REQUEST, getBookmarkForUserWorker)
......
......@@ -79,8 +79,6 @@ describe('bookmark ducks', () => {
expect(reducer(testState, { type: 'INVALID.ACTION' })).toEqual(testState);
expect(reducer(testState, { type: AddBookmark.FAILURE })).toEqual(testState);
expect(reducer(testState, { type: GetBookmarks.FAILURE })).toEqual(testState);
expect(reducer(testState, { type: GetBookmarksForUser.FAILURE })).toEqual(testState);
expect(reducer(testState, { type: GetBookmarksForUser.SUCCESS })).toEqual(testState);
expect(reducer(testState, { type: RemoveBookmark.FAILURE })).toEqual(testState);
});
......@@ -278,7 +276,7 @@ describe('sagas', () => {
testResourceKey = 'bookmarked_key';
testResourceType = ResourceType.table;
action = removeBookmark(testResourceKey, testResourceType);
})
});
it('removes a bookmark', () => {
return expectSaga(removeBookmarkWorker, action)
......
......@@ -65,6 +65,5 @@ export interface GetBookmarksForUserResponse {
type: GetBookmarksForUser.SUCCESS | GetBookmarksForUser.FAILURE;
payload: {
bookmarks: Bookmark[];
userId: string;
};
}
......@@ -34,7 +34,7 @@ import {
import { getAllTagsWatcher } from './allTags/sagas';
// User
import { getLoggedInUserWatcher, getUserWatcher } from "./user/sagas";
import { getLoggedInUserWatcher, getUserOwnWatcher, getUserReadWatcher, getUserWatcher } from "./user/sagas";
export default function* rootSaga() {
yield all([
......@@ -66,5 +66,7 @@ export default function* rootSaga() {
// User
getLoggedInUserWatcher(),
getUserWatcher(),
getUserOwnWatcher(),
getUserReadWatcher(),
]);
}
import axios, { AxiosResponse } from 'axios';
import { LoggedInUser, PeopleUser } from 'interfaces';
import { LoggedInUser, PeopleUser, Resource } from 'interfaces';
export type LoggedInUserResponse = { user: LoggedInUser; msg: string; };
export type UserResponse = { user: PeopleUser; msg: string; };
export type UserOwnResponse = { own: Resource[], msg: string; };
export type UserReadResponse = { read: Resource[], msg: string; };
export function getLoggedInUser() {
return axios.get(`/api/auth_user`)
......@@ -14,7 +16,21 @@ export function getLoggedInUser() {
export function getUserById(userId: string) {
return axios.get(`/api/metadata/v0/user?user_id=${userId}`)
.then((response: AxiosResponse<UserResponse>) => {
return response.data.user;
});
.then((response: AxiosResponse<UserResponse>) => {
return response.data.user;
});
}
export function getUserOwn(userId: string) {
return axios.get(`/api/metadata/v0/user/own?user_id=${userId}`)
.then((response: AxiosResponse<UserOwnResponse>) => {
return response.data
});
}
export function getUserRead(userId: string) {
return axios.get(`/api/metadata/v0/user/read?user_id=${userId}`)
.then((response: AxiosResponse<UserReadResponse>) => {
return response.data
});
}
import { LoggedInUser, PeopleUser } from 'interfaces';
import { LoggedInUser, PeopleUser, Resource } from 'interfaces';
import {
GetLoggedInUser,
......@@ -7,8 +7,16 @@ import {
GetUser,
GetUserRequest,
GetUserResponse,
GetUserOwn, GetUserOwnRequest, GetUserOwnResponse,
GetUserRead, GetUserReadRequest, GetUserReadResponse,
} from './types';
type UserReducerAction =
GetLoggedInUserRequest | GetLoggedInUserResponse |
GetUserRequest | GetUserResponse |
GetUserOwnRequest | GetUserOwnResponse |
GetUserReadRequest | GetUserReadResponse;
/* ACTIONS */
export function getLoggedInUser(): GetLoggedInUserRequest {
return { type: GetLoggedInUser.REQUEST };
......@@ -17,10 +25,22 @@ export function getUserById(userId: string): GetUserRequest {
return { userId, type: GetUser.REQUEST };
};
export function getUserOwn(userId: string): GetUserOwnRequest {
return { type: GetUserOwn.REQUEST, payload: { userId }};
};
export function getUserRead(userId: string): GetUserReadRequest {
return { type: GetUserRead.REQUEST, payload: { userId }};
};
/* REDUCER */
export interface UserReducerState {
loggedInUser: LoggedInUser;
profileUser: PeopleUser;
profile: {
own: Resource[],
read: Resource[],
user: PeopleUser,
};
};
const defaultUser = {
......@@ -41,18 +61,72 @@ const defaultUser = {
};
const initialState: UserReducerState = {
loggedInUser: defaultUser,
profileUser: defaultUser,
profile: {
own: [],
read: [],
user: defaultUser,
},
};
export default function reducer(state: UserReducerState = initialState, action): UserReducerState {
export default function reducer(state: UserReducerState = initialState, action: UserReducerAction): UserReducerState {
switch (action.type) {
case GetLoggedInUser.SUCCESS:
return { ...state, loggedInUser: (<GetLoggedInUserResponse>action).payload.user };
return {
...state,
loggedInUser: action.payload.user,
};
case GetUser.REQUEST:
case GetUser.FAILURE:
return { ...state, profileUser: defaultUser };
return {
...state,
profile: {
...state.profile,
user: defaultUser,
},
};
case GetUser.SUCCESS:
return { ...state, profileUser: (<GetUserResponse>action).payload.user };
return {
...state,
profile: {
...state.profile,
user: action.payload.user,
},
};
case GetUserOwn.REQUEST:
case GetUserOwn.FAILURE:
return {
...state,
profile: {
...state.profile,
own: [],
}
};
case GetUserOwn.SUCCESS:
return {
...state,
profile: {
...state.profile,
...action.payload,
}
};
case GetUserRead.REQUEST:
case GetUserRead.FAILURE:
return {
...state,
profile: {
...state.profile,
read: [],
}
};
case GetUserRead.SUCCESS:
return {
...state,
profile: {
...state.profile,
...action.payload,
}
};
default:
return state;
}
......
import { SagaIterator } from 'redux-saga';
import { call, put, takeEvery } from 'redux-saga/effects';
import { GetLoggedInUser, GetUser, GetUserRequest } from './types';
import { getLoggedInUser, getUserById } from './api/v0';
import {
GetLoggedInUser,
GetUser,
GetUserOwn,
GetUserOwnRequest,
GetUserRead,
GetUserReadRequest,
GetUserRequest
} from './types';
import { getLoggedInUser, getUserById, getUserOwn, getUserRead } from './api/v0';
export function* getLoggedInUserWorker(): SagaIterator {
try {
......@@ -21,9 +29,35 @@ export function* getUserWorker(action: GetUserRequest): SagaIterator {
const user = yield call(getUserById, action.userId);
yield put({ type: GetUser.SUCCESS, payload: { user } });
} catch (e) {
yield put({ type: GetUser.FAILURE});
yield put({ type: GetUser.FAILURE });
}
};
export function* getUserWatcher(): SagaIterator {
yield takeEvery(GetUser.REQUEST, getUserWorker);
}
export function* getUserOwnWorker(action: GetUserOwnRequest): SagaIterator {
try {
const userOwn = yield call(getUserOwn, action.payload.userId);
yield put({ type: GetUserOwn.SUCCESS, payload: { own: userOwn.own }});
} catch (e) {
yield put({ type: GetUserOwn.FAILURE })
}
};
export function* getUserOwnWatcher(): SagaIterator {
yield takeEvery(GetUserOwn.REQUEST, getUserOwnWorker);
};
export function* getUserReadWorker(action: GetUserReadRequest): SagaIterator {
try {
const userRead = yield call(getUserRead, action.payload.userId);
yield put({ type: GetUserRead.SUCCESS, payload: { read: userRead.read }});
} catch (e) {
yield put({ type: GetUserRead.FAILURE })
}
};
export function* getUserReadWatcher(): SagaIterator {
yield takeEvery(GetUserRead.REQUEST, getUserReadWorker);
};
import { LoggedInUser, PeopleUser } from 'interfaces';
import { LoggedInUser, PeopleUser, Resource } from 'interfaces';
export enum GetLoggedInUser {
REQUEST = 'amundsen/current_user/GET_REQUEST',
......@@ -30,3 +31,46 @@ export interface GetUserResponse {
user: PeopleUser;
};
};
/* getUserOwn */
export enum GetUserOwn {
REQUEST = 'amundsen/user/own/GET_REQUEST',
SUCCESS = 'amundsen/user/own/GET_SUCCESS',
FAILURE = 'amundsen/user/own/GET_FAILURE',
}
export interface GetUserOwnRequest {
type: GetUserOwn.REQUEST;
payload: {
userId: string;
};
};
export interface GetUserOwnResponse {
type: GetUserOwn.SUCCESS | GetUserOwn.FAILURE;
payload?: {
own: Resource[];
};
};
/* getUserRead */
export enum GetUserRead {
REQUEST = 'amundsen/user/read/GET_REQUEST',
SUCCESS = 'amundsen/user/read/GET_SUCCESS',
FAILURE = 'amundsen/user/read/GET_FAILURE',
}
export interface GetUserReadRequest {
type: GetUserRead.REQUEST;
payload: {
userId: string;
};
};
export interface GetUserReadResponse {
type: GetUserRead.SUCCESS | GetUserRead.FAILURE;
payload?: {
read: Resource[];
};
};
......@@ -136,21 +136,32 @@ const globalState: GlobalState = {
team_name: 'QA',
user_id: 'test0',
},
profileUser: {
display_name: 'firstname lastname',
email: 'test@test.com',
employee_type: 'fulltime',
first_name: 'firstname',
full_name: 'firstname lastname',
github_username: 'githubName',
is_active: true,
last_name: 'lastname',
manager_fullname: 'Test Manager',
profile_url: 'www.test.com',
role_name: 'Tester',
slack_id: 'www.slack.com',
team_name: 'QA',
user_id: 'test0',
profile: {
own: [
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
],
read: [
{ type: ResourceType.table },
{ type: ResourceType.table },
],
user: {
display_name: 'firstname lastname',
email: 'test@test.com',
employee_type: 'fulltime',
first_name: 'firstname',
full_name: 'firstname lastname',
github_username: 'githubName',
is_active: true,
last_name: 'lastname',
manager_fullname: 'Test Manager',
profile_url: 'www.test.com',
role_name: 'Tester',
slack_id: 'www.slack.com',
team_name: 'QA',
user_id: 'test0',
},
},
},
};
......
......@@ -81,4 +81,5 @@ export interface UserResource extends Resource {
title: string;
}
// TODO - Consider just using the 'Resource' type instead
export type Bookmark = TableResource & {};
......@@ -201,7 +201,7 @@ class MetadataTest(unittest.TestCase):
'team_name': 'Amundsen',
'user_id': 'testuserid',
}
self.get_bookmark_response = {
self.get_user_resource_response = {
'table': [
{
'cluster': 'cluster',
......@@ -219,7 +219,7 @@ class MetadataTest(unittest.TestCase):
},
]
}
self.expected_parsed_bookmarks = [
self.expected_parsed_user_resources = [
{
'cluster': 'cluster',
'database': 'database',
......@@ -561,13 +561,13 @@ class MetadataTest(unittest.TestCase):
Test get_bookmark with no user specified
"""
url = '{0}{1}/{2}/follow/'.format(local_app.config['METADATASERVICE_BASE'], USER_ENDPOINT, TEST_USER_ID)
responses.add(responses.GET, url, json=self.get_bookmark_response, status=HTTPStatus.OK)
responses.add(responses.GET, url, json=self.get_user_resource_response, status=HTTPStatus.OK)
with local_app.test_client() as test:
response = test.get('/api/metadata/v0/user/bookmark')
data = json.loads(response.data)
self.assertEquals(response.status_code, HTTPStatus.OK)
self.assertCountEqual(data.get('bookmarks'), self.expected_parsed_bookmarks)
self.assertCountEqual(data.get('bookmarks'), self.expected_parsed_user_resources)
@responses.activate
def test_get_bookmark_for_user(self) -> None:
......@@ -576,13 +576,13 @@ class MetadataTest(unittest.TestCase):
"""
specified_user = 'other_user'
url = '{0}{1}/{2}/follow/'.format(local_app.config['METADATASERVICE_BASE'], USER_ENDPOINT, specified_user)
responses.add(responses.GET, url, json=self.get_bookmark_response, status=HTTPStatus.OK)
responses.add(responses.GET, url, json=self.get_user_resource_response, status=HTTPStatus.OK)
with local_app.test_client() as test:
response = test.get('/api/metadata/v0/user/bookmark', query_string=dict(user_id=specified_user))
data = json.loads(response.data)
self.assertEquals(response.status_code, HTTPStatus.OK)
self.assertCountEqual(data.get('bookmarks'), self.expected_parsed_bookmarks)
self.assertCountEqual(data.get('bookmarks'), self.expected_parsed_user_resources)
@responses.activate
def test_put_bookmark(self) -> None:
......@@ -631,3 +631,33 @@ class MetadataTest(unittest.TestCase):
})
self.assertEquals(response.status_code, HTTPStatus.OK)
@responses.activate
def test_get_user_read(self) -> None:
"""
Test get_user_read API request
"""
test_user = 'test_user'
url = '{0}{1}/{2}/read/'.format(local_app.config['METADATASERVICE_BASE'], USER_ENDPOINT, test_user)
responses.add(responses.GET, url, json=self.get_user_resource_response, status=HTTPStatus.OK)
with local_app.test_client() as test:
response = test.get('/api/metadata/v0/user/read', query_string=dict(user_id=test_user))
data = json.loads(response.data)
self.assertEquals(response.status_code, HTTPStatus.OK)
self.assertCountEqual(data.get('read'), self.expected_parsed_user_resources)
@responses.activate
def test_get_user_own(self) -> None:
"""
Test get_user_own API request
"""
test_user = 'test_user'
url = '{0}{1}/{2}/own/'.format(local_app.config['METADATASERVICE_BASE'], USER_ENDPOINT, test_user)
responses.add(responses.GET, url, json=self.get_user_resource_response, status=HTTPStatus.OK)
with local_app.test_client() as test:
response = test.get('/api/metadata/v0/user/own', query_string=dict(user_id=test_user))
data = json.loads(response.data)
self.assertEquals(response.status_code, HTTPStatus.OK)
self.assertCountEqual(data.get('own'), self.expected_parsed_user_resources)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment