mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-01-06 12:36:56 +00:00
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:
parent
c586bb1aed
commit
6a48403662
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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 (
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -12,3 +12,4 @@
|
||||
*/
|
||||
|
||||
export const UrlEntityCharRegEx = /[#.%;?/\\]/g;
|
||||
export const validEmailRegEx = /^\S+@\S+\.\S+$/;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user