diff --git a/moove/api/src/main/kotlin/io/charlescd/moove/api/controller/V2UserController.kt b/moove/api/src/main/kotlin/io/charlescd/moove/api/controller/V2UserController.kt index ff12f5a951..74a7db7524 100644 --- a/moove/api/src/main/kotlin/io/charlescd/moove/api/controller/V2UserController.kt +++ b/moove/api/src/main/kotlin/io/charlescd/moove/api/controller/V2UserController.kt @@ -71,7 +71,7 @@ class V2UserController( @RequestHeader(value = "Authorization") authorization: String, @RequestParam("name", required = false) name: String?, @RequestParam("email", required = false) email: String?, - pageable: PageRequest + @Valid pageable: PageRequest ): ResourcePageResponse { return this.findAllUsersInteractor.execute(name, email, authorization, pageable) } diff --git a/ui/src/core/components/LabeledIcon/__tests__/LabeledIcon.spec.tsx b/ui/src/core/components/LabeledIcon/__tests__/LabeledIcon.spec.tsx index 05b6a325c4..5bd3d07539 100644 --- a/ui/src/core/components/LabeledIcon/__tests__/LabeledIcon.spec.tsx +++ b/ui/src/core/components/LabeledIcon/__tests__/LabeledIcon.spec.tsx @@ -33,7 +33,7 @@ test('renders LabeledIcon component with default properties', () => { expect(textElement).toBeInTheDocument(); expect(labelElement).toBeInTheDocument(); - expect(labelElement).toHaveStyle('margin-left: 5px;'); + expect(labelElement).toHaveStyle('margin-left:8px;'); expect(iconElement).toBeInTheDocument(); }); diff --git a/ui/src/core/components/LabeledIcon/index.tsx b/ui/src/core/components/LabeledIcon/index.tsx index 919969f00b..7d895859f2 100644 --- a/ui/src/core/components/LabeledIcon/index.tsx +++ b/ui/src/core/components/LabeledIcon/index.tsx @@ -34,7 +34,7 @@ const LabeledIcon = ({ className, size = '15px', isActive, - marginContent = '5px', + marginContent = '8px', onClick }: Props) => { return ( diff --git a/ui/src/core/providers/users.ts b/ui/src/core/providers/users.ts index c81ca69b3e..cf8005749f 100644 --- a/ui/src/core/providers/users.ts +++ b/ui/src/core/providers/users.ts @@ -24,12 +24,15 @@ const endpointWorkspaces = '/moove/v2/workspaces/users'; const v1Endpoint = '/moove/users'; export interface UserFilter { + id?: string; name?: string; email?: string; + page?: number; } const initialUserFilter = { - email: '' + name: '', + page: 0 }; export const findAllWorkspaceUsers = ( @@ -48,10 +51,9 @@ export const findAllWorkspaceUsers = ( }; export const findAllUsers = (filter: UserFilter = initialUserFilter) => { - const defaultPage = 0; const params = new URLSearchParams({ size: `${DEFAULT_PAGE_SIZE}`, - page: `${defaultPage}` + page: `${filter.page ?? 0}` }); if (filter?.name) params.append('name', filter?.name); diff --git a/ui/src/core/state/__tests__/hooks.spec.tsx b/ui/src/core/state/__tests__/hooks.spec.ts similarity index 70% rename from ui/src/core/state/__tests__/hooks.spec.tsx rename to ui/src/core/state/__tests__/hooks.spec.ts index 3797f36d3d..25c322c75e 100644 --- a/ui/src/core/state/__tests__/hooks.spec.tsx +++ b/ui/src/core/state/__tests__/hooks.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import React from 'react'; import { renderHook } from '@testing-library/react-hooks'; import { useGlobalState, useDispatch } from '../hooks'; import { AllTheProviders as wrapper } from 'unit-test/testUtils'; import { circlesInitialState } from 'modules/Circles/state/reducer'; +import { userInitialState } from 'modules/Users/state/reducer'; -test('useGlobalState', () => { +test('circle: useGlobalState', () => { const { result } = renderHook(() => useGlobalState(state => state.circles), { wrapper }); @@ -28,7 +28,21 @@ test('useGlobalState', () => { expect(result.current).toEqual(circlesInitialState); }); -test('useDispatch', () => { +test('circle: useDispatch', () => { + const { result } = renderHook(() => useDispatch(), { wrapper }); + + expect(result.current).toEqual(expect.any(Function)); +}); + +test('users: useGlobalState', () => { + const { result } = renderHook(() => useGlobalState(state => state.users), { + wrapper + }); + + expect(result.current).toEqual(userInitialState); +}); + +test('users: useDispatch', () => { const { result } = renderHook(() => useDispatch(), { wrapper }); expect(result.current).toEqual(expect.any(Function)); diff --git a/ui/src/modules/Account/Menu/MenuItem.tsx b/ui/src/modules/Account/Menu/MenuItem.tsx index 8aa27fdcb5..8e18b69622 100644 --- a/ui/src/modules/Account/Menu/MenuItem.tsx +++ b/ui/src/modules/Account/Menu/MenuItem.tsx @@ -39,7 +39,7 @@ const MenuItem = ({ id, icon, name, path }: Props) => { isActive={isActive(id)} data-testid={`menu-${name}`} > - + {name} diff --git a/ui/src/modules/Circles/Menu/Loaders/MenuItem.spec.tsx b/ui/src/modules/Circles/Menu/__tests__/MenuItem.spec.tsx similarity index 100% rename from ui/src/modules/Circles/Menu/Loaders/MenuItem.spec.tsx rename to ui/src/modules/Circles/Menu/__tests__/MenuItem.spec.tsx diff --git a/ui/src/modules/Groups/__tests__/fixtures.ts b/ui/src/modules/Groups/__tests__/fixtures.ts new file mode 100644 index 0000000000..976a83803a --- /dev/null +++ b/ui/src/modules/Groups/__tests__/fixtures.ts @@ -0,0 +1,55 @@ +export const userGroupPagination = { + content: [{ + id: '123', + name: 'group 1', + users: [{ + id: '123', + name: 'Charles', + email: 'charlescd@zup.com.br', + photoUrl: 'https://charlescd.io', + createdAt: '2020-01-01 12:00' + }] + }], + page: 0, + size: 0, + totalPages: 0, + last: true +}; + +export const userGroup = { + id: '123', + name: 'group 1', + author: { + id: '456', + name: 'Charles', + email: 'charlescd@zup.com.br', + createdAt: '2020-01-01 12:00', + }, + createdAt: '2020-01-01 12:00', + users: [{ + id: '123', + name: 'Charles', + email: 'charlescd@zup.com.br', + photoUrl: 'https://charlescd.io', + createdAt: '2020-01-01 12:00' + }] +}; + +export const users = { + content: [{ + id: '123', + name: 'Charles', + email: 'charlescd@zup.com.br', + photoUrl: 'https://charlescd.io', + applications: [{ + id: '123', + name: 'Application 1', + menbersCount: 1 + }], + createdAt: '2020-01-01 12:00' + }], + page: 0, + size: 0, + totalPages: 0, + last: true +}; \ No newline at end of file diff --git a/ui/src/modules/Groups/__tests__/hooks.spec.tsx b/ui/src/modules/Groups/__tests__/hooks.spec.ts similarity index 72% rename from ui/src/modules/Groups/__tests__/hooks.spec.tsx rename to ui/src/modules/Groups/__tests__/hooks.spec.ts index cd9246b8f3..108522b781 100644 --- a/ui/src/modules/Groups/__tests__/hooks.spec.tsx +++ b/ui/src/modules/Groups/__tests__/hooks.spec.ts @@ -19,6 +19,7 @@ import { screen, wait } from 'unit-test/testUtils'; import { FetchMock } from 'jest-fetch-mock'; import { useCreateUserGroup, useDeleteUserGroup, useFindAllUserGroup, useFindUserGroupByID, useListUser, useManagerMemberInUserGroup, useUpdateUserGroup } from '../hooks'; import { UserGroup } from '../interfaces/UserGroups'; +import {userGroupPagination, userGroup, users} from './fixtures'; beforeEach(() => { (fetch as FetchMock).resetMocks(); @@ -65,24 +66,6 @@ test('error create a new user group', async () => { }); test('to find all user groups', async () => { - const userGroupPagination = { - content: [{ - id: '123', - name: 'group 1', - users: [{ - id: '123', - name: 'Charles', - email: 'charlescd@zup.com.br', - photoUrl: 'https://charlescd.io', - createdAt: '2020-01-01 12:00' - }] - }], - page: 0, - size: 0, - totalPages: 0, - last: true - }; - (fetch as FetchMock).mockResponseOnce(JSON.stringify(userGroupPagination)); const { result } = renderHook(() => useFindAllUserGroup()); @@ -95,25 +78,6 @@ test('to find all user groups', async () => { }); test('to find user group by id', async () => { - const userGroup = { - id: '123', - name: 'group 1', - author: { - id: '456', - name: 'Charles', - email: 'charlescd@zup.com.br', - createdAt: '2020-01-01 12:00', - }, - createdAt: '2020-01-01 12:00', - users: [{ - id: '123', - name: 'Charles', - email: 'charlescd@zup.com.br', - photoUrl: 'https://charlescd.io', - createdAt: '2020-01-01 12:00' - }] - }; - (fetch as FetchMock).mockResponseOnce(JSON.stringify(userGroup)); const { result } = renderHook(() => useFindUserGroupByID()); @@ -126,25 +90,6 @@ test('to find user group by id', async () => { }); test('to list users', async () => { - const users = { - content: [{ - id: '123', - name: 'Charles', - email: 'charlescd@zup.com.br', - photoUrl: 'https://charlescd.io', - applications: [{ - id: '123', - name: 'Application 1', - menbersCount: 1 - }], - createdAt: '2020-01-01 12:00' - }], - page: 0, - size: 0, - totalPages: 0, - last: true - }; - (fetch as FetchMock).mockResponseOnce(JSON.stringify(users)); const { result } = renderHook(() => useListUser()); @@ -157,25 +102,6 @@ test('to list users', async () => { }); test('to update user group', async () => { - const userGroup = { - id: '123', - name: 'group 1', - author: { - id: '456', - name: 'Charles', - email: 'charlescd@zup.com.br', - createdAt: '2020-01-01 12:00', - }, - createdAt: '2020-01-01 12:00', - users: [{ - id: '123', - name: 'Charles', - email: 'charlescd@zup.com.br', - photoUrl: 'https://charlescd.io', - createdAt: '2020-01-01 12:00' - }] - }; - (fetch as FetchMock).mockResponseOnce(JSON.stringify(userGroup)); const { result } = renderHook(() => useUpdateUserGroup()); diff --git a/ui/src/modules/Hypotheses/Menu/MenuItem.tsx b/ui/src/modules/Hypotheses/Menu/MenuItem.tsx index e794ca3533..6a982fda15 100644 --- a/ui/src/modules/Hypotheses/Menu/MenuItem.tsx +++ b/ui/src/modules/Hypotheses/Menu/MenuItem.tsx @@ -36,11 +36,7 @@ const MenuItem = ({ id, name, onClick }: Props) => { return ( onClick()} isActive={isActive}> - + {name} diff --git a/ui/src/modules/Metrics/Menu/MenuItem.tsx b/ui/src/modules/Metrics/Menu/MenuItem.tsx index 5b35a05f09..eb8b18ed2c 100644 --- a/ui/src/modules/Metrics/Menu/MenuItem.tsx +++ b/ui/src/modules/Metrics/Menu/MenuItem.tsx @@ -33,7 +33,7 @@ const MenuItem = ({ route, id, name }: Props) => { return ( history.push(route)} isActive={isActive}> - + {name} diff --git a/ui/src/modules/Modules/Menu/MenuItem.tsx b/ui/src/modules/Modules/Menu/MenuItem.tsx index 0d4adc92e0..a5f0c2cf08 100644 --- a/ui/src/modules/Modules/Menu/MenuItem.tsx +++ b/ui/src/modules/Modules/Menu/MenuItem.tsx @@ -46,11 +46,7 @@ const MenuItem = ({ id, name }: Props) => { return ( onMenuClick()} isActive={isActive()}> - + {moduleFormatterName(name)} diff --git a/ui/src/modules/Settings/Menu/MenuItem.tsx b/ui/src/modules/Settings/Menu/MenuItem.tsx index d871fbbc3c..e874fe3ef5 100644 --- a/ui/src/modules/Settings/Menu/MenuItem.tsx +++ b/ui/src/modules/Settings/Menu/MenuItem.tsx @@ -39,7 +39,7 @@ const MenuItem = ({ id, icon, name, path }: Props) => { isActive={isActive(id)} data-testid={`menu-item-link-${name}`} > - + {name} diff --git a/ui/src/modules/Users/Menu/Loaders/index.tsx b/ui/src/modules/Users/Menu/Loaders/index.tsx index 7a814eedbb..b5a4965ee1 100644 --- a/ui/src/modules/Users/Menu/Loaders/index.tsx +++ b/ui/src/modules/Users/Menu/Loaders/index.tsx @@ -15,10 +15,10 @@ */ import React from 'react'; -import { Loader as LoaderList } from './list'; +import { Loader as LoaderList, Props as ListProps } from './list'; const Loader = { - List: () => + List: ({ className }: ListProps) => }; export default Loader; diff --git a/ui/src/modules/Users/Menu/Loaders/list.tsx b/ui/src/modules/Users/Menu/Loaders/list.tsx index 56e304eb00..7003b83c2c 100644 --- a/ui/src/modules/Users/Menu/Loaders/list.tsx +++ b/ui/src/modules/Users/Menu/Loaders/list.tsx @@ -14,10 +14,14 @@ * limitations under the License. */ -import React, { FunctionComponent } from 'react'; +import React from 'react'; import ContentLoader from 'react-content-loader'; -export const Loader: FunctionComponent = () => ( +export type Props = { + className?: string; +}; + +export const Loader = ({ className }: Props) => ( ( viewBox="0 0 200 200" backgroundColor="#3a393c" foregroundColor="#2c2b2e" + className={className} > diff --git a/ui/src/modules/Users/Menu/MenuItem.tsx b/ui/src/modules/Users/Menu/MenuItem.tsx index a28b69303e..aa3ac5a656 100644 --- a/ui/src/modules/Users/Menu/MenuItem.tsx +++ b/ui/src/modules/Users/Menu/MenuItem.tsx @@ -17,24 +17,38 @@ import React from 'react'; import Text from 'core/components/Text'; import Styled from './styled'; +import { useHistory } from 'react-router-dom'; +import useQueryStrings from 'core/utils/query'; +import { addParam, delParam } from 'core/utils/path'; +import routes from 'core/constants/routes'; interface Props { id: string; name: string; - isActive: boolean; - onSelect: () => void; + email: string; } -const MenuItem = ({ id, name, isActive, onSelect }: Props) => ( - - - {name} - - -); +const MenuItem = ({ id, name, email }: Props) => { + const history = useHistory(); + const query = useQueryStrings(); + const isActive = () => query.getAll('user').includes(id); + + const toggleUser = () => + isActive() + ? delParam('user', routes.usersComparation, history, id) + : addParam('user', routes.usersComparation, history, id); + + return ( + toggleUser()} + isActive={isActive()} + data-testid={`menu-users-${email}`} + > + + {name} + + + ); +}; export default MenuItem; diff --git a/ui/src/modules/Users/Menu/__tests__/Menu.spec.tsx b/ui/src/modules/Users/Menu/__tests__/Menu.spec.tsx index c002ba2e1f..69a4351b3b 100644 --- a/ui/src/modules/Users/Menu/__tests__/Menu.spec.tsx +++ b/ui/src/modules/Users/Menu/__tests__/Menu.spec.tsx @@ -15,9 +15,8 @@ */ import React from 'react'; -import { render, screen, waitFor } from 'unit-test/testUtils'; +import { render, screen } from 'unit-test/testUtils'; import { FetchMock } from 'jest-fetch-mock/types'; -import userEvent from '@testing-library/user-event'; import Menu from '..'; const props = { @@ -43,21 +42,14 @@ beforeEach(() => { }); test('render Menu default', async () => { - render( - - ); + const onSearch = jest.fn(); + const props = { + children: 'button' + }; - expect(screen.getByTestId('menu-users-charles@zup.com.br')).toBeInTheDocument(); -}); - -test('render Menu default and do a empty search', async () => { - render( - - ); - - const inputSearch = screen.getByTestId('input-text-search'); + render(); - userEvent.type(inputSearch, 'unknown'); - - await waitFor(() => expect(screen.getByTestId('empty-result-user')).toBeInTheDocument()); + expect(screen.getByTestId('icon-plus-circle')).toBeInTheDocument(); + expect(screen.getByText('Create user')).toBeInTheDocument(); + expect(screen.getByTestId('input-text-search')).toBeInTheDocument(); }); diff --git a/ui/src/modules/Users/Menu/__tests__/MenuItem.spec.tsx b/ui/src/modules/Users/Menu/__tests__/MenuItem.spec.tsx new file mode 100644 index 0000000000..26a0f47174 --- /dev/null +++ b/ui/src/modules/Users/Menu/__tests__/MenuItem.spec.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2020 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render, screen } from 'unit-test/testUtils'; +import userEvent from '@testing-library/user-event'; +import * as pathUtils from 'core/utils/path'; +import routes from 'core/constants/routes'; +import MenuItem from '../MenuItem'; + +test('should render MenuItem', async () => { + const addParamSpy = jest.spyOn(pathUtils, 'addParam'); + + render(); + + userEvent.click(screen.getByTestId('menu-users-charlesadmin@admin')); + + expect(screen.getByTestId('icon-user')).toBeInTheDocument(); + expect(screen.getByText('charlesadmin')).toBeInTheDocument(); + + expect(addParamSpy).toHaveBeenCalledWith( + 'user', + routes.usersComparation, + expect.anything(), + '1' + ); +}); diff --git a/ui/src/modules/Users/Menu/index.tsx b/ui/src/modules/Users/Menu/index.tsx index 21d5fdd2cd..13c91c4dee 100644 --- a/ui/src/modules/Users/Menu/index.tsx +++ b/ui/src/modules/Users/Menu/index.tsx @@ -14,27 +14,21 @@ * limitations under the License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { useHistory } from 'react-router-dom'; -import map from 'lodash/map'; -import isEmpty from 'lodash/isEmpty'; import Text from 'core/components/Text'; import LabeledIcon from 'core/components/LabeledIcon'; import routes from 'core/constants/routes'; -import { UserPaginationItem } from '../interfaces/UserPagination'; -import MenuItem from './MenuItem'; import Styled from './styled'; -import Loader from './Loaders'; import useQueryStrings from 'core/utils/query'; import { addParam, delParam } from 'core/utils/path'; interface Props { - items: UserPaginationItem[]; onSearch: (name: string) => void; - isLoading: boolean; + children: ReactNode; } -const UserMenu = ({ items, onSearch, isLoading }: Props) => { +const UserMenu = ({ onSearch, children }: Props) => { const history = useHistory(); const query = useQueryStrings(); @@ -45,38 +39,17 @@ const UserMenu = ({ items, onSearch, isLoading }: Props) => { ? delParam('user', routes.usersComparation, history, id) : addParam('user', routes.usersComparation, history, id); - const renderUsers = () => - isEmpty(items) ? ( - - No User was found - - ) : ( - map(items, ({ email, name }: UserPaginationItem) => ( - toggleUser(email)} - /> - )) - ); - return ( <> - toggleUser('create')} isActive={false}> + toggleUser('create')}> Create user - + - - - - {isEmpty(items) && isLoading ? : renderUsers()} - - + + {children} ); }; diff --git a/ui/src/modules/Users/Menu/styled.ts b/ui/src/modules/Users/Menu/styled.ts index f796ffb49c..7c625c8ccf 100644 --- a/ui/src/modules/Users/Menu/styled.ts +++ b/ui/src/modules/Users/Menu/styled.ts @@ -18,7 +18,9 @@ import styled from 'styled-components'; import LabeledIcon from 'core/components/LabeledIcon'; import SearchInputComponent from 'core/components/Form/SearchInput'; import IconComponent from 'core/components/Icon'; +import ButtonComponent from 'core/components/Button'; import { COLOR_BLACK_MARLIN } from 'core/assets/colors'; +import Text from 'core/components/Text'; const SearchInput = styled(SearchInputComponent)` margin: 15px 0; @@ -40,33 +42,30 @@ const Icon = styled(IconComponent)` `; const Content = styled.div` - height: calc(100vh - 200px); + height: calc(-200px + 100vh); overflow-y: auto; `; -const List = styled.ul` - display: flex; - flex-direction: column; - margin: 0; - padding: 0; - list-style-type: none; - - > * { - padding: 0 16px; - } -`; - const ListItem = styled(LabeledIcon)` padding: 15px 0px; cursor: pointer; display: flex; `; +const Item = styled(Text.h4)` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 230px; +`; interface LinkProps { isActive: boolean; } const Link = styled('button')` + width: 100%; + display: block; + padding: 0 16px; background: none; border: none; text-decoration: none; @@ -78,13 +77,22 @@ const A = styled.a` text-decoration: none; `; +const Button = styled(ButtonComponent.Default)` + border: none; + background-color: transparent; + padding: 0; + margin: 0; + height: auto; +`; + export default { A, Actions, + Button, Content, Icon, Link, - List, ListItem, + Item, SearchInput }; diff --git a/ui/src/modules/Users/__tests__/Users.spec.tsx b/ui/src/modules/Users/__tests__/Users.spec.tsx new file mode 100644 index 0000000000..495732bd23 --- /dev/null +++ b/ui/src/modules/Users/__tests__/Users.spec.tsx @@ -0,0 +1,28 @@ +/* + * Copyright 2020 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render, screen } from 'unit-test/testUtils'; +import Users from '..'; + +test('should render Users', async () => { + render(); + + expect(screen.getByTestId('page')).toBeInTheDocument(); + expect(screen.getByTestId('page-menu')).toBeInTheDocument(); + expect(screen.getByTestId('input-text-search')).toBeInTheDocument(); + expect(screen.getByText('Create user')).toBeInTheDocument(); +}); \ No newline at end of file diff --git a/ui/src/modules/Users/__tests__/fixtures.ts b/ui/src/modules/Users/__tests__/fixtures.ts new file mode 100644 index 0000000000..a3470bcd18 --- /dev/null +++ b/ui/src/modules/Users/__tests__/fixtures.ts @@ -0,0 +1,15 @@ +export const userPagination = { + content: [ + { + id: '123', + name: 'charlesadmin', + email: 'charlesadmin@admin', + applications: [''], + createdAt: '01/29/2021' + } + ], + page: 0, + size: 0, + totalPages: 0, + last: true +}; \ No newline at end of file diff --git a/ui/src/modules/Users/__tests__/hooks.spec.tsx b/ui/src/modules/Users/__tests__/hooks.spec.ts similarity index 83% rename from ui/src/modules/Users/__tests__/hooks.spec.tsx rename to ui/src/modules/Users/__tests__/hooks.spec.ts index 03e963db52..2756041f6c 100644 --- a/ui/src/modules/Users/__tests__/hooks.spec.tsx +++ b/ui/src/modules/Users/__tests__/hooks.spec.ts @@ -17,8 +17,9 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { waitFor } from 'unit-test/testUtils'; import { FetchMock } from 'jest-fetch-mock'; -import { useCreateUser, useUpdateName, useUser, useWorkspacesByUser } from '../hooks'; +import { useCreateUser, useUpdateName, useUser, useWorkspacesByUser, useUsers } from '../hooks'; import { NewUser, User } from '../interfaces/User'; +import {userPagination} from './fixtures'; beforeEach(() => { (fetch as FetchMock).resetMocks(); @@ -173,4 +174,36 @@ test('should throw an error in userWorkspacesByUser', async () => { }); expect(response).toBeUndefined(); +}); + +// TODO hooks.spec.tsx to .ts +// TODO no test usergroup, colocar data em fixture file +test('should find all users', async () => { + (fetch as FetchMock).mockResponse(JSON.stringify(userPagination)); + + const { result } = renderHook(() => useUsers()); + + const name = ''; + const page = 0; + + await act(async () => { + await result.current[0](name, page); + }); + + await waitFor(() => expect(result.current[1]).toMatchObject(userPagination)); +}); + +test('should get an error when finding all users', async () => { + (fetch as FetchMock).mockRejectedValue(new Response(JSON.stringify({}))); + + const { result } = renderHook(() => useUsers()); + + const name = ''; + const page = 0; + + await act(async () => { + await result.current[0](name, page); + }); + + await waitFor(() => expect(result.current[1]).toBeUndefined()); }); \ No newline at end of file diff --git a/ui/src/modules/Users/hooks.ts b/ui/src/modules/Users/hooks.ts index 94d94f6f19..2ef7ff3ae0 100644 --- a/ui/src/modules/Users/hooks.ts +++ b/ui/src/modules/Users/hooks.ts @@ -278,14 +278,14 @@ export const useResetPassword = (): { return { resetPassword, response, status }; }; -export const useUsers = (): [Function, Function, boolean] => { +export const useUsers = (): [Function, UserPagination, boolean] => { const dispatch = useDispatch(); const [usersData, getUsers] = useFetch(findAllUsers); const { response, error, loading } = usersData; - const getAll = useCallback( - (name: string) => { - getUsers({ name }); + const filterUsers = useCallback( + (name: string, page: number) => { + getUsers({ name, page }); }, [getUsers] ); @@ -298,7 +298,7 @@ export const useUsers = (): [Function, Function, boolean] => { } }, [dispatch, response, error]); - return [getAll, getUsers, loading]; + return [filterUsers, response, loading]; }; export default useUsers; diff --git a/ui/src/modules/Users/index.tsx b/ui/src/modules/Users/index.tsx index 662fdb8fc9..1f011e0a42 100644 --- a/ui/src/modules/Users/index.tsx +++ b/ui/src/modules/Users/index.tsx @@ -18,13 +18,17 @@ import React, { lazy, useState, useEffect, Suspense } from 'react'; import { Route, Switch } from 'react-router-dom'; import isEmpty from 'lodash/isEmpty'; import Page from 'core/components/Page'; -import { useGlobalState } from 'core/state/hooks'; import routes from 'core/constants/routes'; import { getProfileByKey } from 'core/utils/profile'; import getQueryStrings from 'core/utils/query'; import Menu from './Menu'; import { useUsers } from './hooks'; import Styled from './styled'; +import InfiniteScroll from 'core/components/InfiniteScroll'; +import { useDispatch, useGlobalState } from 'core/state/hooks'; +import { resetContentAction } from './state/actions'; +import map from 'lodash/map'; +import MenuItem from './Menu/MenuItem'; const UsersComparation = lazy(() => import('./Comparation')); @@ -32,19 +36,25 @@ const CreateUser = lazy(() => import('./Create')); const Users = () => { const profileName = getProfileByKey('name'); - const [getAll, , loading] = useUsers(); + const [filterUsers, , loading] = useUsers(); const [name, setName] = useState(''); const [message, setMessage] = useState(''); const { list } = useGlobalState(({ users }) => users); const query = getQueryStrings(); const users = query.getAll('user'); + const dispatch = useDispatch(); useEffect(() => { - getAll(name); - if (message === 'Deleted' || message === 'Created') { - getAll(name); + const page = 0; + dispatch(resetContentAction()); + if (message === '' || message === 'Deleted') { + filterUsers(name, page); } - }, [name, getAll, message]); + }, [name, message, filterUsers, dispatch]); + + const loadMore = (page: number) => { + filterUsers(name, page); + }; const renderPlaceholder = () => ( { /> ); + const renderUsers = () => + map(list?.content, ({ email, name }) => ( + + )); + return ( - + + } + > + {renderUsers()} + + diff --git a/ui/src/modules/Users/state/actions.ts b/ui/src/modules/Users/state/actions.ts index bf285d48f0..2f0d07097f 100644 --- a/ui/src/modules/Users/state/actions.ts +++ b/ui/src/modules/Users/state/actions.ts @@ -19,7 +19,8 @@ import { User } from '../interfaces/User'; export enum ACTION_TYPES { loadedUsers = 'USERS/LOADED_USERS', - loadedUser = 'USERS/LOADED_USER' + loadedUser = 'USERS/LOADED_USER', + resetContent = 'USERS/RESET_CONTENT' } interface LoadedUsersActionType { @@ -43,5 +44,15 @@ export const LoadedUserAction = (payload: User): UsersActionTypes => ({ type: ACTION_TYPES.loadedUser, payload }); +interface ResetContentActionType { + type: typeof ACTION_TYPES.resetContent; +} + +export const resetContentAction = (): ResetContentActionType => ({ + type: ACTION_TYPES.resetContent +}); -export type UsersActionTypes = LoadedUsersActionType | LoadedUserActionType; +export type UsersActionTypes = + | LoadedUsersActionType + | LoadedUserActionType + | ResetContentActionType; diff --git a/ui/src/modules/Users/state/reducer.ts b/ui/src/modules/Users/state/reducer.ts index cd7379fc19..c9f22ae3e2 100644 --- a/ui/src/modules/Users/state/reducer.ts +++ b/ui/src/modules/Users/state/reducer.ts @@ -39,7 +39,10 @@ export const userReducer = ( case ACTION_TYPES.loadedUsers: { return { ...state, - list: action.payload + list: { + ...action.payload, + content: [...state.list.content, ...(action?.payload?.content ?? [])] + } }; } case ACTION_TYPES.loadedUser: { @@ -48,6 +51,15 @@ export const userReducer = ( item: action.payload }; } + case ACTION_TYPES.resetContent: { + return { + ...state, + list: { + ...state.list, + content: [] + } + }; + } default: { return state; } diff --git a/ui/src/modules/Users/styled.ts b/ui/src/modules/Users/styled.ts index 44a8796d2e..0d4451a034 100644 --- a/ui/src/modules/Users/styled.ts +++ b/ui/src/modules/Users/styled.ts @@ -16,12 +16,18 @@ import styled from 'styled-components'; import Page from 'core/components/Page'; +import LoaderMenuComponent from './Menu/Loaders'; const ScrollableX = styled(Page.Content)` overflow-y: hidden; overflow-x: auto; `; +const LoaderMenu = styled(LoaderMenuComponent.List)` + margin-left: 16px; +`; + export default { - ScrollableX + ScrollableX, + LoaderMenu }; diff --git a/ui/src/modules/Workspaces/Menu/MenuItem.tsx b/ui/src/modules/Workspaces/Menu/MenuItem.tsx index 6192baf255..edc1f0024b 100644 --- a/ui/src/modules/Workspaces/Menu/MenuItem.tsx +++ b/ui/src/modules/Workspaces/Menu/MenuItem.tsx @@ -57,7 +57,7 @@ const MenuItem = ({ id, name, status, selectedWorkspace }: Props) => { return ( - + {name}