Feat: Add form to create new user from ui, Fix issue-3480 [UI] Default Value for User's not Assigned to any Teams on User's Listing Page (#3494)

* Feat: Add form to create new user.

* rename adduser to create usear everywhere

* Addressing comments
This commit is contained in:
Shailesh Parmar 2022-03-18 19:38:46 +05:30 committed by GitHub
parent c586bb1aed
commit 6a48403662
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 695 additions and 45 deletions

View File

@ -15,6 +15,7 @@ import { AxiosResponse } from 'axios';
import { Operation } from 'fast-json-patch';
import { UserProfile } from 'Models';
import { SearchIndex } from '../enums/search.enum';
import { CreateUser } from '../generated/api/teams/createUser';
import { User } from '../generated/entity/teams/user';
import { getURLWithQueryFields } from '../utils/APIUtils';
import APIClient from './index';
@ -88,9 +89,9 @@ export const getUserById: Function = (id: string): Promise<AxiosResponse> => {
return APIClient.get(`/users/${id}`);
};
export const createUser = (userDetails: {
[name: string]: string | Array<string> | UserProfile;
}): Promise<AxiosResponse> => {
export const createUser = (
userDetails: Record<string, string | Array<string> | UserProfile> | CreateUser
): Promise<AxiosResponse> => {
return APIClient.post(`/users`, userDetails);
};

View File

@ -0,0 +1,325 @@
/*
* Copyright 2021 Collate
* 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import { cloneDeep, isEmpty, isUndefined } from 'lodash';
import { EditorContentRef } from 'Models';
import React, { useRef, useState } from 'react';
import { validEmailRegEx } from '../../constants/regex.constants';
import { CreateUser as CreateUserSchema } from '../../generated/api/teams/createUser';
import { Role } from '../../generated/entity/teams/role';
import { EntityReference as UserTeams } from '../../generated/entity/teams/user';
import jsonData from '../../jsons/en';
import { errorMsg, requiredField } from '../../utils/CommonUtils';
import { Button } from '../buttons/Button/Button';
import MarkdownWithPreview from '../common/editor/MarkdownWithPreview';
import PageLayout from '../containers/PageLayout';
import DropDown from '../dropdown/DropDown';
import { DropDownListItem } from '../dropdown/types';
import { Field } from '../Field/Field';
import Loader from '../Loader/Loader';
import { CreateUserProps } from './CreateUser.interface';
const CreateUser = ({
allowAccess,
roles,
teams,
saveState = 'initial',
onCancel,
onSave,
}: CreateUserProps) => {
const markdownRef = useRef<EditorContentRef>();
const [description] = useState<string>('');
const [email, setEmail] = useState('');
const [isAdmin, setIsAdmin] = useState(false);
const [isBot, setIsBot] = useState(false);
const [selectedRoles, setSelectedRoles] = useState<Array<string | undefined>>(
[]
);
const [selectedTeams, setSelectedTeams] = useState<Array<string | undefined>>(
[]
);
const [showErrorMsg, setShowErrorMsg] = useState({
email: false,
validEmail: false,
});
/**
* common function to update user input in to the state
* @param event change event for input/selection field
* @returns if user dont have access to the page it will not update data.
*/
const handleValidation = (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
if (!allowAccess) {
return;
}
const value = event.target.value;
const eleName = event.target.name;
switch (eleName) {
case 'email':
setEmail(value);
setShowErrorMsg({ ...showErrorMsg, email: false });
break;
default:
break;
}
};
/**
* Generate DropdownListItem
* @param data Array containing object which must have name and id
* @returns DropdownListItem[]
*/
const getDropdownOptions = (
data: Array<Role> | Array<UserTeams>
): DropDownListItem[] => {
return [
...data.map((option) => {
return {
name: option.displayName || '',
value: option.id,
};
}),
];
};
/**
* Dropdown option selector
* @param id of selected option from dropdown
*/
const selectedRolesHandler = (id?: string) => {
setSelectedRoles((prevState: Array<string | undefined>) => {
if (prevState.includes(id as string)) {
const selectedRole = [...prevState];
const index = selectedRole.indexOf(id as string);
selectedRole.splice(index, 1);
return selectedRole;
} else {
return [...prevState, id];
}
});
};
/**
* Dropdown option selector.
* @param id of selected option from dropdown.
*/
const selectedTeamsHandler = (id?: string) => {
setSelectedTeams((prevState: Array<string | undefined>) => {
if (prevState.includes(id as string)) {
const selectedRole = [...prevState];
const index = selectedRole.indexOf(id as string);
selectedRole.splice(index, 1);
return selectedRole;
} else {
return [...prevState, id];
}
});
};
/**
* Validate if required value is provided or not.
* @returns boolean
*/
const validateForm = () => {
const errMsg = cloneDeep(showErrorMsg);
if (isEmpty(email)) {
errMsg.email = true;
} else {
errMsg.validEmail = !validEmailRegEx.test(email);
}
setShowErrorMsg(errMsg);
return !Object.values(errMsg).includes(true);
};
/**
* Form submit handler
*/
const handleSave = () => {
const validRole = selectedRoles.filter(
(id) => !isUndefined(id)
) as string[];
const validTeam = selectedTeams.filter(
(id) => !isUndefined(id)
) as string[];
if (validateForm()) {
const userProfile: CreateUserSchema = {
description: markdownRef.current?.getEditorContent() || undefined,
name: email.split('@')[0],
roles: validRole.length ? validRole : undefined,
teams: validTeam.length ? validTeam : undefined,
email: email,
isAdmin: isAdmin,
isBot: isBot,
};
onSave(userProfile);
}
};
/**
* Dynamic button provided as per its state, useful for micro interaction
* @returns Button
*/
const getSaveButton = () => {
return allowAccess ? (
<>
{saveState === 'waiting' ? (
<Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular"
theme="primary"
variant="contained">
<Loader size="small" type="white" />
</Button>
) : saveState === 'success' ? (
<Button
disabled
className="tw-w-16 tw-h-10 disabled:tw-opacity-100"
size="regular"
theme="primary"
variant="contained">
<FontAwesomeIcon icon="check" />
</Button>
) : (
<Button
className={classNames('tw-w-16 tw-h-10', {
'tw-opacity-40': !allowAccess,
})}
data-testid="save-user"
size="regular"
theme="primary"
variant="contained"
onClick={handleSave}>
Create
</Button>
)}
</>
) : null;
};
return (
<PageLayout classes="tw-max-w-full-hd tw-h-full tw-bg-white tw-py-4">
<h6 className="tw-heading tw-text-base">Create User</h6>
<Field>
<label className="tw-block tw-form-label tw-mb-0" htmlFor="email">
{requiredField('Email:')}
</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
data-testid="email"
id="email"
name="email"
placeholder="email"
type="text"
value={email}
onChange={handleValidation}
/>
{showErrorMsg.email
? errorMsg(jsonData['form-error-messages']['empty-email'])
: showErrorMsg.validEmail
? errorMsg(jsonData['form-error-messages']['invalid-email'])
: null}
</Field>
<Field>
<label className="tw-block tw-form-label tw-mb-0" htmlFor="description">
Description:
</label>
<MarkdownWithPreview ref={markdownRef} value={description} />
</Field>
<Field>
<label className="tw-block tw-form-label tw-mb-0">Teams:</label>
<DropDown
className={classNames('tw-bg-white', {
'tw-bg-gray-100 tw-cursor-not-allowed': teams.length === 0,
})}
dropDownList={getDropdownOptions(teams) as DropDownListItem[]}
label="Teams"
selectedItems={selectedTeams as Array<string>}
type="checkbox"
onSelect={(_e, value) => selectedTeamsHandler(value)}
/>
</Field>
<Field>
<label className="tw-block tw-form-label tw-mb-0" htmlFor="role">
Roles:
</label>
<DropDown
className={classNames('tw-bg-white', {
'tw-bg-gray-100 tw-cursor-not-allowed': roles.length === 0,
})}
dropDownList={getDropdownOptions(roles) as DropDownListItem[]}
label="Roles"
selectedItems={selectedRoles as Array<string>}
type="checkbox"
onSelect={(_e, value) => selectedRolesHandler(value)}
/>
</Field>
<Field className="tw-flex tw-gap-5">
<div className="tw-flex tw-pt-1">
<label>Admin</label>
<div
className={classNames('toggle-switch', { open: isAdmin })}
data-testid="admin"
onClick={() => {
if (allowAccess) {
setIsAdmin((prev) => !prev);
setIsBot(false);
}
}}>
<div className="switch" />
</div>
</div>
<div className="tw-flex tw-pt-1">
<label>Bot</label>
<div
className={classNames('toggle-switch', { open: isBot })}
data-testid="bot"
onClick={() => {
if (allowAccess) {
setIsBot((prev) => !prev);
setIsAdmin(false);
}
}}>
<div className="switch" />
</div>
</div>
</Field>
<Field className="tw-flex tw-justify-end">
<Button
data-testid="cancel-user"
size="regular"
theme="primary"
variant="text"
onClick={onCancel}>
Discard
</Button>
{getSaveButton()}
</Field>
</PageLayout>
);
};
export default CreateUser;

View File

@ -0,0 +1,26 @@
/*
* Copyright 2021 Collate
* 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 { LoadingState } from 'Models';
import { CreateUser } from '../../generated/api/teams/createUser';
import { Role } from '../../generated/entity/teams/role';
import { EntityReference as UserTeams } from '../../generated/entity/teams/user';
export interface CreateUserProps {
allowAccess: boolean;
saveState?: LoadingState;
roles: Array<Role>;
teams: Array<UserTeams>;
onSave: (data: CreateUser) => void;
onCancel: () => void;
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2021 Collate
* 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 {
findAllByText,
findByTestId,
findByText,
render,
} from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import CreateUser from './CreateUser.component';
import { CreateUserProps } from './CreateUser.interface';
jest.mock(
'../containers/PageLayout',
() =>
({ children }: { children: React.ReactNode }) =>
<div data-testid="PageLayout">{children}</div>
);
jest.mock('../dropdown/DropDown', () => {
return jest.fn().mockReturnValue(<p>Dropdown component</p>);
});
jest.mock('../common/editor/MarkdownWithPreview', () => {
return jest.fn().mockReturnValue(<p>MarkdownWithPreview component</p>);
});
const propsValue: CreateUserProps = {
allowAccess: true,
saveState: 'initial',
roles: [],
teams: [],
onSave: jest.fn(),
onCancel: jest.fn(),
};
describe('Test CreateUser component', () => {
it('CreateUser component should render properly', async () => {
const { container } = render(<CreateUser {...propsValue} />, {
wrapper: MemoryRouter,
});
const PageLayout = await findByTestId(container, 'PageLayout');
const email = await findByTestId(container, 'email');
const admin = await findByTestId(container, 'admin');
const bot = await findByTestId(container, 'bot');
const cancelButton = await findByTestId(container, 'cancel-user');
const saveButton = await findByTestId(container, 'save-user');
const description = await findByText(
container,
/MarkdownWithPreview component/i
);
const dropdown = await findAllByText(container, /Dropdown component/i);
expect(PageLayout).toBeInTheDocument();
expect(email).toBeInTheDocument();
expect(bot).toBeInTheDocument();
expect(admin).toBeInTheDocument();
expect(description).toBeInTheDocument();
expect(dropdown.length).toBe(2);
expect(cancelButton).toBeInTheDocument();
expect(saveButton).toBeInTheDocument();
});
});

View File

@ -11,17 +11,21 @@
* limitations under the License.
*/
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { compare, Operation } from 'fast-json-patch';
import { isUndefined, toLower } from 'lodash';
import React, { FunctionComponent, useEffect, useState } from 'react';
import React, { Fragment, FunctionComponent, useEffect, useState } from 'react';
import PageLayout from '../../components/containers/PageLayout';
import Loader from '../../components/Loader/Loader';
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
import { UserType } from '../../enums/user.enum';
import { Role } from '../../generated/entity/teams/role';
import { Team } from '../../generated/entity/teams/team';
import { User } from '../../generated/entity/teams/user';
import { getCountBadge } from '../../utils/CommonUtils';
import { Button } from '../buttons/Button/Button';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import Searchbar from '../common/searchbar/Searchbar';
import UserDetailsModal from '../Modals/UserDetailsModal/UserDetailsModal';
import UserDataCard from '../UserDataCard/UserDataCard';
@ -31,6 +35,7 @@ interface Props {
roles: Array<Role>;
allUsers: Array<User>;
updateUser: (id: string, data: Operation[], updatedUser: User) => void;
handleAddUserClick: () => void;
isLoading: boolean;
}
@ -38,6 +43,7 @@ const UserList: FunctionComponent<Props> = ({
allUsers = [],
isLoading,
updateUser,
handleAddUserClick,
teams = [],
roles = [],
}: Props) => {
@ -231,47 +237,64 @@ const UserList: FunctionComponent<Props> = ({
const getLeftPanel = () => {
return (
<div className="tw-mt-5">
<div
className="tw-flex tw-items-center tw-justify-between tw-mb-2 tw-cursor-pointer"
onClick={() => {
selectTeam();
}}>
<div
className={`tw-group tw-text-grey-body tw-text-body tw-flex tw-justify-between ${getCurrentTeamClass()}`}>
<p className="tw-text-center tag-category tw-self-center">
All Users
</p>
</div>
{getCountBadge(allUsers.length || 0, '', isTeamBadgeActive())}
<Fragment>
<div className="tw-flex tw-justify-between tw-items-center tw-pt-2 tw-border-b">
<h6 className="tw-heading tw-text-base">Users</h6>
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
<Button
className="tw-h-7 tw-px-2 tw-mb-3"
data-testid="add-teams"
size="small"
theme="primary"
variant="contained"
onClick={handleAddUserClick}>
<FontAwesomeIcon icon="plus" />
</Button>
</NonAdminAction>
</div>
{teams &&
teams.map((team: Team) => (
<div className="tw-mt-5">
<div
className="tw-flex tw-items-center tw-justify-between tw-mb-2 tw-cursor-pointer"
onClick={() => {
selectTeam();
}}>
<div
className="tw-flex tw-items-center tw-justify-between tw-mb-2 tw-cursor-pointer"
key={team.name}
onClick={() => {
selectTeam(team);
setSearchText('');
}}>
<div
className={`tw-group tw-text-grey-body tw-text-body tw-flex tw-justify-between ${getCurrentTeamClass(
team.name
)}`}>
<p
className="tag-category tw-self-center tw-truncate tw-w-48"
title={team.displayName}>
{team.displayName}
</p>
</div>
{getCountBadge(
team.users?.length || 0,
'',
isTeamBadgeActive(team.name)
)}
className={`tw-group tw-text-grey-body tw-text-body tw-flex tw-justify-between ${getCurrentTeamClass()}`}>
<p className="tw-text-center tag-category tw-self-center">
All Users
</p>
</div>
))}
</div>
{getCountBadge(allUsers.length || 0, '', isTeamBadgeActive())}
</div>
{teams &&
teams.map((team: Team) => (
<div
className="tw-flex tw-items-center tw-justify-between tw-mb-2 tw-cursor-pointer"
key={team.name}
onClick={() => {
selectTeam(team);
setSearchText('');
}}>
<div
className={`tw-group tw-text-grey-body tw-text-body tw-flex tw-justify-between ${getCurrentTeamClass(
team.name
)}`}>
<p
className="tag-category tw-self-center tw-truncate tw-w-48"
title={team.displayName}>
{team.displayName}
</p>
</div>
{getCountBadge(
team.users?.length || 0,
'',
isTeamBadgeActive(team.name)
)}
</div>
))}
</div>
</Fragment>
);
};
@ -308,9 +331,11 @@ const UserList: FunctionComponent<Props> = ({
isActiveUser: !user.deleted,
profilePhoto: user.profile?.images?.image || '',
teamCount:
user.teams
?.map((team) => team.displayName ?? team.name)
?.join(', ') ?? '',
user.teams && user.teams?.length
? user.teams
?.map((team) => team.displayName ?? team.name)
?.join(', ')
: 'No teams',
};
return (

View File

@ -157,6 +157,7 @@ export const ROUTES = {
PIPELINE_DETAILS: `/pipeline/${PLACEHOLDER_ROUTE_PIPELINE_FQN}`,
PIPELINE_DETAILS_WITH_TAB: `/pipeline/${PLACEHOLDER_ROUTE_PIPELINE_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
USER_LIST: '/user-list',
CREATE_USER: '/create-user',
USER_PROFILE: `/users/${PLACEHOLDER_USER_NAME}`,
ROLES: '/roles',
WEBHOOKS: '/webhooks',

View File

@ -12,3 +12,4 @@
*/
export const UrlEntityCharRegEx = /[#.%;?/\\]/g;
export const validEmailRegEx = /^\S+@\S+\.\S+$/;

View File

@ -15,6 +15,7 @@ const jsonData = {
'api-error-messages': {
'add-glossary-error': 'Error while adding glossary!',
'add-glossary-term-error': 'Error while adding glossary term!',
'create-user-error': 'Error while creating user!',
'delete-glossary-error': 'Error while deleting glossary!',
'delete-glossary-term-error': 'Error while deleting glossary term!',
'delete-team-error': 'Error while deleting team!',
@ -27,6 +28,10 @@ const jsonData = {
'update-glossary-term-error': 'Error while updating glossary term!',
'update-description-error': 'Error while updating description!',
},
'form-error-messages': {
'empty-email': 'Email is required.',
'invalid-email': 'Email is invalid.',
},
};
export default jsonData;

View File

@ -0,0 +1,116 @@
/*
* Copyright 2021 Collate
* 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 { AxiosError } from 'axios';
import { observer } from 'mobx-react';
import { LoadingState } from 'Models';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import AppState from '../../AppState';
import { useAuthContext } from '../../auth-provider/AuthProvider';
import { createUser } from '../../axiosAPIs/userAPI';
import PageContainerV1 from '../../components/containers/PageContainerV1';
import CreateUserComponent from '../../components/CreateUser/CreateUser.component';
import { ROUTES } from '../../constants/constants';
import { CreateUser } from '../../generated/api/teams/createUser';
import { Role } from '../../generated/entity/teams/role';
import { EntityReference as UserTeams } from '../../generated/entity/teams/user';
import { useAuth } from '../../hooks/authHooks';
import useToastContext from '../../hooks/useToastContext';
import jsonData from '../../jsons/en';
const CreateUserPage = () => {
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const showToast = useToastContext();
const history = useHistory();
const [roles, setRoles] = useState<Array<Role>>([]);
const [teams, setTeams] = useState<Array<UserTeams>>([]);
const [status, setStatus] = useState<LoadingState>('initial');
const goToUserListPage = () => {
history.push(ROUTES.USER_LIST);
};
const handleCancel = () => {
goToUserListPage();
};
/**
* Creates toast notification for error.
* @param errMessage Error message
*/
const handleShowErrorToast = (errMessage: string) => {
showToast({
variant: 'error',
body: errMessage,
});
};
/**
* Handles error if any, while creating new user.
* @param errorMessage Error message
*/
const handleSaveFailure = (errorMessage = '') => {
handleShowErrorToast(
errorMessage || jsonData['api-error-messages']['create-user-error']
);
setStatus('initial');
};
/**
* Submit handler for new user form.
* @param userData Data for creating new user
*/
const handleAddUserSave = (userData: CreateUser) => {
setStatus('waiting');
createUser(userData)
.then((res) => {
if (res.data) {
setStatus('success');
setTimeout(() => {
setStatus('initial');
goToUserListPage();
}, 500);
} else {
handleSaveFailure();
}
})
.catch((err: AxiosError) => {
handleSaveFailure(err.response?.data?.message);
});
};
useEffect(() => {
setRoles(AppState.userRoles);
}, [AppState.userRoles]);
useEffect(() => {
setTeams(AppState.userTeams);
}, [AppState.userTeams]);
return (
<PageContainerV1>
<CreateUserComponent
allowAccess={isAdminUser || isAuthDisabled}
roles={roles}
saveState={status}
teams={teams}
onCancel={handleCancel}
onSave={handleAddUserSave}
/>
</PageContainerV1>
);
};
export default observer(CreateUserPage);

View File

@ -0,0 +1,61 @@
/*
* Copyright 2021 Collate
* 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 { findByTestId, findByText, render } from '@testing-library/react';
import React, { ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import AddUserPageComponent from './CreateUserPage.component';
jest.mock('../../components/containers/PageContainerV1', () => {
return jest
.fn()
.mockImplementation(({ children }: { children: ReactNode }) => (
<div data-testid="PageContainerV1">{children}</div>
));
});
jest.mock('../../auth-provider/AuthProvider', () => ({
useAuthContext: jest.fn().mockReturnValue({ isAuthDisabled: true }),
}));
jest.mock('../../hooks/authHooks', () => ({
useAuth: jest.fn().mockReturnValue({ isAdminUser: true }),
}));
jest.mock('../../components/CreateUser/CreateUser.component', () => {
return jest.fn().mockReturnValue(<div>CreateUser component</div>);
});
jest.mock('../../AppState', () =>
jest.fn().mockReturnValue({
userRoles: [],
userTeams: [],
})
);
describe('Test AddUserPage component', () => {
it('AddUserPage component should render properly', async () => {
const { container } = render(<AddUserPageComponent />, {
wrapper: MemoryRouter,
});
const pageContainerV1 = await findByTestId(container, 'PageContainerV1');
const createUserComponent = await findByText(
container,
/CreateUser component/i
);
expect(pageContainerV1).toBeInTheDocument();
expect(createUserComponent).toBeInTheDocument();
});
});

View File

@ -15,11 +15,13 @@ import { AxiosError, AxiosResponse } from 'axios';
import { Operation } from 'fast-json-patch';
import { observer } from 'mobx-react';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import AppState from '../../AppState';
import { getTeams } from '../../axiosAPIs/teamsAPI';
import { updateUserDetail } from '../../axiosAPIs/userAPI';
import PageContainerV1 from '../../components/containers/PageContainerV1';
import UserList from '../../components/UserList/UserList';
import { ROUTES } from '../../constants/constants';
import { Role } from '../../generated/entity/teams/role';
import { Team } from '../../generated/entity/teams/team';
import { User } from '../../generated/entity/teams/user';
@ -27,6 +29,7 @@ import useToastContext from '../../hooks/useToastContext';
const UserListPage = () => {
const showToast = useToastContext();
const history = useHistory();
const [teams, setTeams] = useState<Array<Team>>([]);
const [roles, setRoles] = useState<Array<Role>>([]);
@ -50,6 +53,13 @@ const UserListPage = () => {
});
};
/**
* Redirect user to add-user route for adding new user.
*/
const handleAddUserClick = () => {
history.push(ROUTES.CREATE_USER);
};
const updateUser = (id: string, data: Operation[], updatedUser: User) => {
setIsLoading(true);
updateUserDetail(id, data)
@ -84,6 +94,7 @@ const UserListPage = () => {
<PageContainerV1>
<UserList
allUsers={allUsers}
handleAddUserClick={handleAddUserClick}
isLoading={isLoading}
roles={roles}
teams={teams}

View File

@ -21,6 +21,7 @@ import { ROUTES } from '../constants/constants';
import { useAuth } from '../hooks/authHooks';
import AddGlossaryPage from '../pages/AddGlossary/AddGlossaryPage.component';
import AddWebhookPage from '../pages/AddWebhookPage/AddWebhookPage.component';
import CreateUserPage from '../pages/CreateUserPage/CreateUserPage.component';
import DashboardDetailsPage from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
import DatabaseDetails from '../pages/database-details/index';
import DatasetDetailsPage from '../pages/DatasetDetailsPage/DatasetDetailsPage.component';
@ -130,6 +131,7 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
/>
<Route exact component={AddWebhookPage} path={ROUTES.ADD_WEBHOOK} />
<Route exact component={RolesPage} path={ROUTES.ROLES} />
<Route exact component={CreateUserPage} path={ROUTES.CREATE_USER} />
<Route exact component={UserListPage} path={ROUTES.USER_LIST} />
</>
) : null}