Feat #7770 UI : Allow user to edit botUser Details on Bot profile page. (#7771)

This commit is contained in:
Sachin Chaurasiya 2022-10-03 05:16:19 +05:30 committed by GitHub
parent 819188cfc1
commit c27657ea5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 650 additions and 317 deletions

View File

@ -65,14 +65,9 @@ describe('Bots Page should work properly', () => {
cy.get('[data-testid="displayName"]').should('exist').type(botName);
//Enter description
cy.get(descriptionBox).type(description);
//Generate Password
interceptURL('GET', ' /api/v1/users/generateRandomPwd', 'generatePassword');
cy.get('[data-testid="password-generator"]').should('be.visible').click();
verifyResponseStatusCode('@generatePassword', 200);
cy.wait(1000);
//Click on save button
cy.wait(1000);
interceptURL('POST', '/api/v1/bots', 'createBot');
interceptURL('PUT', '/api/v1/bots', 'createBot');
cy.get('[data-testid="save-user"]')
.scrollIntoView()
.should('be.visible')
@ -97,9 +92,8 @@ describe('Bots Page should work properly', () => {
.type(updatedBotName);
//Save the updated display name
interceptURL('GET', '/api/v1/users/auth-mechanism/*', 'getBotDetails');
cy.get('[data-testid="save-displayName"]').should('be.visible').click();
verifyResponseStatusCode('@getBotDetails', 200);
//Verify the display name is updated on bot details page
cy.get('[data-testid="container"]').should('contain', updatedBotName);
cy.wait(1000);

View File

@ -63,3 +63,12 @@ export const deleteBot = async (id: string, hardDelete?: boolean) => {
return response.data;
};
export const createBotWithPut = async (data: CreateBot) => {
const response = await axiosClient.put<CreateBot, AxiosResponse<Bot>>(
BASE_URL,
data
);
return response.data;
};

View File

@ -19,6 +19,7 @@ import {
AuthenticationMechanism,
CreateUser,
} from '../generated/api/teams/createUser';
import { Bot } from '../generated/entity/bot';
import { JwtAuth } from '../generated/entity/teams/authN/jwtAuth';
import { User } from '../generated/entity/teams/user';
import { EntityReference } from '../generated/type/entityReference';
@ -196,3 +197,34 @@ export const getAuthMechanismForBotUser = async (botId: string) => {
return response.data;
};
export const getBotByName = async (name: string, arrQueryFields?: string) => {
const url = getURLWithQueryFields(`/bots/name/${name}`, arrQueryFields);
const response = await APIClient.get<Bot>(url);
return response.data;
};
export const updateBotDetail = async (id: string, data: Operation[]) => {
const configOptions = {
headers: { 'Content-type': 'application/json-patch+json' },
};
const response = await APIClient.patch<Operation[], AxiosResponse<Bot>>(
`/bots/${id}`,
data,
configOptions
);
return response.data;
};
export const createUserWithPut = async (userDetails: CreateUser) => {
const response = await APIClient.put<CreateUser, AxiosResponse<User>>(
`/users`,
userDetails
);
return response.data;
};

View File

@ -15,13 +15,17 @@ import { Button, Divider, Input, Space, Typography } from 'antd';
import { capitalize } from 'lodash';
import React, { FC } from 'react';
import { AuthType } from '../../generated/api/teams/createUser';
import { AuthenticationMechanism } from '../../generated/entity/teams/user';
import {
AuthenticationMechanism,
User,
} from '../../generated/entity/teams/user';
import { getTokenExpiry } from '../../utils/BotsUtils';
import SVGIcons from '../../utils/SvgUtils';
import CopyToClipboardButton from '../buttons/CopyToClipboardButton/CopyToClipboardButton';
import './AuthMechanism.less';
interface Props {
botUser: User;
authenticationMechanism: AuthenticationMechanism;
hasPermission: boolean;
onEdit: () => void;
@ -33,6 +37,7 @@ const AuthMechanism: FC<Props> = ({
hasPermission,
onEdit,
onTokenRevoke,
botUser,
}: Props) => {
if (authenticationMechanism.authType === AuthType.Jwt) {
const JWTToken = authenticationMechanism.config?.JWTToken;
@ -137,6 +142,18 @@ const AuthMechanism: FC<Props> = ({
<Divider style={{ margin: '8px 0px' }} />
<Space className="w-full" direction="vertical">
<>
<Typography.Text>Account Email</Typography.Text>
<Space className="w-full tw-justify-between ant-space-authMechanism">
<Input
contentEditable={false}
data-testid="botUser-email"
value={botUser.email}
/>
<CopyToClipboardButton copyText={botUser.email} />
</Space>
</>
{authConfig?.secretKey && (
<>
<Typography.Text>SecretKey</Typography.Text>

View File

@ -11,35 +11,51 @@
* limitations under the License.
*/
import { Button, Form, Input, Select, Space } from 'antd';
import { Button, Form, Input, Modal, Select, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import { isEmpty } from 'lodash';
import React, { FC, useEffect, useState } from 'react';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { checkEmailInUse } from '../../axiosAPIs/auth-API';
import { createBotWithPut } from '../../axiosAPIs/botsAPI';
import { createUserWithPut, getUserByName } from '../../axiosAPIs/userAPI';
import { BOT_ACCOUNT_EMAIL_CHANGE_CONFIRMATION } from '../../constants/HelperTextUtil';
import { validEmailRegEx } from '../../constants/regex.constants';
import { EntityType } from '../../enums/entity.enum';
import { Bot } from '../../generated/entity/bot';
import { SsoServiceType } from '../../generated/entity/teams/authN/ssoAuth';
import {
AuthenticationMechanism,
AuthType,
JWTTokenExpiry,
User,
} from '../../generated/entity/teams/user';
import { Auth0SSOClientConfig } from '../../generated/security/client/auth0SSOClientConfig';
import { AzureSSOClientConfig } from '../../generated/security/client/azureSSOClientConfig';
import { CustomOidcSSOClientConfig } from '../../generated/security/client/customOidcSSOClientConfig';
import { GoogleSSOClientConfig } from '../../generated/security/client/googleSSOClientConfig';
import { OktaSSOClientConfig } from '../../generated/security/client/oktaSSOClientConfig';
import jsonData from '../../jsons/en';
import { getNameFromEmail } from '../../utils/AuthProvider.util';
import {
getAuthMechanismFormInitialValues,
getAuthMechanismTypeOptions,
getJWTTokenExpiryOptions,
} from '../../utils/BotsUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { SSOClientConfig } from '../CreateUser/CreateUser.interface';
import Loader from '../Loader/Loader';
const { Option } = Select;
interface Props {
botUser: User;
botData: Bot;
isUpdating: boolean;
authenticationMechanism: AuthenticationMechanism;
onSave: (updatedAuthMechanism: AuthenticationMechanism) => void;
onCancel: () => void;
onEmailChange: () => void;
}
const AuthMechanismForm: FC<Props> = ({
@ -47,6 +63,9 @@ const AuthMechanismForm: FC<Props> = ({
onSave,
onCancel,
authenticationMechanism,
botUser,
botData,
onEmailChange,
}) => {
const { authConfig } = useAuthContext();
@ -62,6 +81,11 @@ const AuthMechanismForm: FC<Props> = ({
({} as SSOClientConfig)
);
const [accountEmail, setAccountEmail] = useState<string>(botUser.email);
const [isConfirmationModalOpen, setIsConfirmationModalOpen] =
useState<boolean>(false);
useEffect(() => {
const authType = authenticationMechanism.authType;
const authConfig = authenticationMechanism.config?.authConfig;
@ -114,6 +138,10 @@ const AuthMechanismForm: FC<Props> = ({
email: value,
}));
break;
case 'email':
setAccountEmail(value);
break;
default:
@ -122,6 +150,9 @@ const AuthMechanismForm: FC<Props> = ({
};
const handleSave = () => {
if (accountEmail !== botUser.email) {
setIsConfirmationModalOpen(true);
} else {
const updatedAuthMechanism: AuthenticationMechanism = {
authType: authMechanism,
config:
@ -138,6 +169,61 @@ const AuthMechanismForm: FC<Props> = ({
};
onSave(updatedAuthMechanism);
}
};
const handleBotUpdate = async (response: User) => {
try {
await createBotWithPut({
name: botData.name,
description: botData.description,
displayName: botData.displayName,
botUser: { id: response.id, type: EntityType.USER },
});
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
onEmailChange();
setIsConfirmationModalOpen(false);
}
};
const handleAccountEmailChange = async () => {
try {
const isUserExists = await checkEmailInUse(accountEmail);
if (isUserExists) {
const userResponse = await getUserByName(
getNameFromEmail(accountEmail)
);
handleBotUpdate(userResponse);
} else {
const userResponse = await createUserWithPut({
email: accountEmail,
name: getNameFromEmail(accountEmail),
botName: botData.name,
isBot: true,
authenticationMechanism: {
authType: authMechanism,
config:
authMechanism === AuthType.Jwt
? {
JWTTokenExpiry: tokenExpiry,
}
: {
ssoServiceType: authConfig?.provider as SsoServiceType,
authConfig: {
...ssoClientConfig,
},
},
},
});
handleBotUpdate(userResponse);
}
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsConfirmationModalOpen(false);
}
};
const getSSOConfig = () => {
@ -464,8 +550,13 @@ const AuthMechanismForm: FC<Props> = ({
};
return (
<>
<Form
id="update-auth-mechanism-form"
initialValues={getAuthMechanismFormInitialValues(
authenticationMechanism,
botUser
)}
layout="vertical"
onFinish={handleSave}>
<Form.Item
@ -523,7 +614,30 @@ const AuthMechanismForm: FC<Props> = ({
</Select>
</Form.Item>
)}
{authMechanism === AuthType.Sso && <>{getSSOConfig()}</>}
{authMechanism === AuthType.Sso && (
<>
<Form.Item
label="Email"
name="email"
rules={[
{
pattern: validEmailRegEx,
required: true,
type: 'email',
message: jsonData['form-error-messages']['invalid-email'],
},
]}>
<Input
data-testid="email"
name="email"
placeholder="email"
value={accountEmail}
onChange={handleOnChange}
/>
</Form.Item>
{getSSOConfig()}
</>
)}
<Space className="w-full tw-justify-end" size={4}>
{!isEmpty(authenticationMechanism) && (
<Button data-testid="cancel-edit" type="link" onClick={onCancel}>
@ -539,6 +653,21 @@ const AuthMechanismForm: FC<Props> = ({
</Button>
</Space>
</Form>
{isConfirmationModalOpen && (
<Modal
centered
destroyOnClose
okText="Confirm"
title="Are you sure?"
visible={isConfirmationModalOpen}
onCancel={() => setIsConfirmationModalOpen(false)}
onOk={handleAccountEmailChange}>
<Typography.Text>
{BOT_ACCOUNT_EMAIL_CHANGE_CONFIRMATION} for {botData.name} bot.
</Typography.Text>
</Modal>
)}
</>
);
};

View File

@ -22,20 +22,22 @@ import React, {
useMemo,
useState,
} from 'react';
import { createBotWithPut } from '../../axiosAPIs/botsAPI';
import {
createUserWithPut,
getAuthMechanismForBotUser,
updateUser,
} from '../../axiosAPIs/userAPI';
import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
} from '../../constants/globalSettings.constants';
import { EntityType } from '../../enums/entity.enum';
import { Bot } from '../../generated/entity/bot';
import {
AuthenticationMechanism,
AuthType,
User,
} from '../../generated/entity/teams/user';
import { EntityReference } from '../../generated/type/entityReference';
import { getEntityName } from '../../utils/CommonUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
@ -51,19 +53,23 @@ import AuthMechanism from './AuthMechanism';
import AuthMechanismForm from './AuthMechanismForm';
interface BotsDetailProp extends HTMLAttributes<HTMLDivElement> {
botsData: User;
botUserData: User;
botData: Bot;
botPermission: OperationPermission;
updateBotsDetails: (data: UserDetails) => Promise<void>;
revokeTokenHandler: () => void;
onEmailChange: () => void;
}
const BotDetails: FC<BotsDetailProp> = ({
botsData,
botData,
botUserData,
updateBotsDetails,
revokeTokenHandler,
botPermission,
onEmailChange,
}) => {
const [displayName, setDisplayName] = useState(botsData.displayName);
const [displayName, setDisplayName] = useState(botData.displayName);
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
const [isRevokingToken, setIsRevokingToken] = useState<boolean>(false);
@ -92,7 +98,7 @@ const BotDetails: FC<BotsDetailProp> = ({
const fetchAuthMechanismForBot = async () => {
try {
const response = await getAuthMechanismForBotUser(botsData.id);
const response = await getAuthMechanismForBotUser(botUserData.id);
setAuthenticationMechanism(response);
} catch (error) {
showErrorToast(error as AxiosError);
@ -106,7 +112,6 @@ const BotDetails: FC<BotsDetailProp> = ({
try {
const {
isAdmin,
teams,
timezone,
name,
description,
@ -114,11 +119,9 @@ const BotDetails: FC<BotsDetailProp> = ({
profile,
email,
isBot,
roles,
} = botsData;
const response = await updateUser({
} = botUserData;
const response = await createUserWithPut({
isAdmin,
teams,
timezone,
name,
description,
@ -126,9 +129,8 @@ const BotDetails: FC<BotsDetailProp> = ({
profile,
email,
isBot,
roles,
authenticationMechanism: {
...botsData.authenticationMechanism,
...botUserData.authenticationMechanism,
authType: updatedAuthMechanism.authType,
config:
updatedAuthMechanism.authType === AuthType.Jwt
@ -140,8 +142,15 @@ const BotDetails: FC<BotsDetailProp> = ({
authConfig: updatedAuthMechanism.config?.authConfig,
},
},
} as User);
if (response.data) {
botName: botData.name,
});
if (response) {
await createBotWithPut({
name: botData.name,
description: botData.description,
displayName: botData.displayName,
botUser: { id: response.id, type: EntityType.USER },
});
fetchAuthMechanismForBot();
}
} catch (error) {
@ -159,7 +168,7 @@ const BotDetails: FC<BotsDetailProp> = ({
};
const handleDisplayNameChange = () => {
if (displayName !== botsData.displayName) {
if (displayName !== botData.displayName) {
updateBotsDetails({ displayName: displayName || '' });
}
setIsDisplayNameEdit(false);
@ -243,8 +252,8 @@ const BotDetails: FC<BotsDetailProp> = ({
return (
<div className="tw--ml-5">
<Description
description={botsData.description || ''}
entityName={getEntityName(botsData as unknown as EntityReference)}
description={botData.description || ''}
entityName={getEntityName(botData)}
hasEditAccess={descriptionPermission || editAllPermission}
isEdit={isDescriptionEdit}
onCancel={() => setIsDescriptionEdit(false)}
@ -303,10 +312,10 @@ const BotDetails: FC<BotsDetailProp> = ({
);
useEffect(() => {
if (botsData.id) {
if (botUserData.id) {
fetchAuthMechanismForBot();
}
}, [botsData]);
}, [botUserData]);
return (
<PageLayout
@ -322,7 +331,7 @@ const BotDetails: FC<BotsDetailProp> = ({
GlobalSettingOptions.BOTS
),
},
{ name: botsData.name || '', url: '', activeTitle: true },
{ name: botData.name || '', url: '', activeTitle: true },
]}
/>
}
@ -339,13 +348,17 @@ const BotDetails: FC<BotsDetailProp> = ({
{isAuthMechanismEdit ? (
<AuthMechanismForm
authenticationMechanism={authenticationMechanism}
botData={botData}
botUser={botUserData}
isUpdating={isUpdating}
onCancel={() => setIsAuthMechanismEdit(false)}
onEmailChange={onEmailChange}
onSave={handleAuthMechanismUpdate}
/>
) : (
<AuthMechanism
authenticationMechanism={authenticationMechanism}
botUser={botUserData}
hasPermission={editAllPermission}
onEdit={handleAuthMechanismEdit}
onTokenRevoke={() => setIsRevokingToken(true)}
@ -355,8 +368,11 @@ const BotDetails: FC<BotsDetailProp> = ({
) : (
<AuthMechanismForm
authenticationMechanism={{} as AuthenticationMechanism}
botData={botData}
botUser={botUserData}
isUpdating={isUpdating}
onCancel={() => setIsAuthMechanismEdit(false)}
onEmailChange={onEmailChange}
onSave={handleAuthMechanismUpdate}
/>
)}

View File

@ -20,8 +20,9 @@ import BotDetails from './BotDetails.component';
const revokeTokenHandler = jest.fn();
const updateBotsDetails = jest.fn();
const onEmailChange = jest.fn();
const botsData = {
const botUserData = {
id: 'ea09aed1-0251-4a75-b92a-b65641610c53',
name: 'sachinchaurasiyachotey87',
fullyQualifiedName: 'sachinchaurasiyachotey87',
@ -36,6 +37,26 @@ const botsData = {
deleted: false,
};
const botData = {
id: '4755f87d-2a53-4376-97e6-fc072f29cf5a',
name: 'ingestion-bot',
fullyQualifiedName: 'ingestion-bot',
displayName: 'ingestion-bot',
botUser: {
id: 'b91d42cb-2a02-4364-ae80-db08b77f1b0c',
type: 'user',
name: 'ingestion-bot',
fullyQualifiedName: 'ingestion-bot',
deleted: false,
href: 'http://localhost:8585/api/v1/users/b91d42cb-2a02-4364-ae80-db08b77f1b0c',
},
version: 0.1,
updatedAt: 1664267598781,
updatedBy: 'ingestion-bot',
href: 'http://localhost:8585/api/v1/bots/4755f87d-2a53-4376-97e6-fc072f29cf5a',
deleted: false,
};
const mockAuthMechanism = {
config: {
JWTToken:
@ -48,7 +69,8 @@ const mockAuthMechanism = {
};
const mockProp = {
botsData,
botUserData,
botData,
botPermission: {
Create: true,
Delete: true,
@ -60,6 +82,7 @@ const mockProp = {
} as OperationPermission,
revokeTokenHandler,
updateBotsDetails,
onEmailChange,
};
jest.mock('../../utils/PermissionsUtils', () => ({
@ -68,7 +91,9 @@ jest.mock('../../utils/PermissionsUtils', () => ({
jest.mock('../../axiosAPIs/userAPI', () => {
return {
updateUser: jest.fn().mockImplementation(() => Promise.resolve(botsData)),
createUserWithPut: jest
.fn()
.mockImplementation(() => Promise.resolve(botUserData)),
getAuthMechanismForBotUser: jest
.fn()
.mockImplementation(() => Promise.resolve(mockAuthMechanism)),

View File

@ -24,9 +24,12 @@ import {
PAGE_SIZE_LARGE,
} from '../../constants/constants';
import { BOTS_DOCS } from '../../constants/docs.constants';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import {
INGESTION_BOT_CANT_BE_DELETED,
NO_PERMISSION_FOR_ACTION,
} from '../../constants/HelperTextUtil';
import { EntityType } from '../../enums/entity.enum';
import { Bot } from '../../generated/entity/bot';
import { Bot, BotType } from '../../generated/entity/bot';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { Include } from '../../generated/type/include';
import { Paging } from '../../generated/type/paging';
@ -114,11 +117,9 @@ const BotListV1 = ({
render: (_, record) => (
<Link
className="hover:tw-underline tw-cursor-pointer"
data-testid={`bot-link-${getEntityName(record?.botUser)}`}
to={getBotsPath(
record?.botUser?.fullyQualifiedName || record?.botUser?.name || ''
)}>
{getEntityName(record?.botUser)}
data-testid={`bot-link-${getEntityName(record)}`}
to={getBotsPath(record?.fullyQualifiedName || record?.name || '')}>
{getEntityName(record)}
</Link>
),
},
@ -127,10 +128,8 @@ const BotListV1 = ({
dataIndex: 'description',
key: 'description',
render: (_, record) =>
record?.botUser?.description ? (
<RichTextEditorPreviewer
markdown={record?.botUser?.description || ''}
/>
record?.description ? (
<RichTextEditorPreviewer markdown={record?.description || ''} />
) : (
<span data-testid="no-description">No Description</span>
),
@ -140,14 +139,21 @@ const BotListV1 = ({
dataIndex: 'id',
key: 'id',
width: 90,
render: (_, record) => (
render: (_, record) => {
const isIngestionBot = record.botType === BotType.IngestionBot;
const title = isIngestionBot
? INGESTION_BOT_CANT_BE_DELETED
: deletePermission
? 'Delete'
: NO_PERMISSION_FOR_ACTION;
const isDisabled = !deletePermission || isIngestionBot;
return (
<Space align="center" size={8}>
<Tooltip
placement="bottom"
title={deletePermission ? 'Delete' : NO_PERMISSION_FOR_ACTION}>
<Tooltip placement="bottom" title={title}>
<Button
data-testid={`bot-delete-${getEntityName(record?.botUser)}`}
disabled={!deletePermission}
data-testid={`bot-delete-${getEntityName(record)}`}
disabled={isDisabled}
icon={
<SVGIcons
alt="Delete"
@ -160,7 +166,8 @@ const BotListV1 = ({
/>
</Tooltip>
</Space>
),
);
},
},
],
[]

View File

@ -28,7 +28,7 @@ import { isUndefined } from 'lodash';
import { EditorContentRef } from 'Models';
import React, { useMemo, useRef, useState } from 'react';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { generateRandomPwd } from '../../axiosAPIs/auth-API';
import { checkEmailInUse, generateRandomPwd } from '../../axiosAPIs/auth-API';
import { getBotsPagePath, getUsersPagePath } from '../../constants/constants';
import { passwordErrorMessage } from '../../constants/error-message';
import {
@ -321,11 +321,6 @@ const CreateUser = ({
email: email,
isAdmin: isAdmin,
isBot: isBot,
password: isPasswordGenerated ? generatedPassword : password,
confirmPassword: isPasswordGenerated
? generatedPassword
: confirmPassword,
createPasswordType: CreatePasswordType.Admincreate,
...(forceBot
? {
authenticationMechanism: {
@ -343,7 +338,13 @@ const CreateUser = ({
},
},
}
: {}),
: {
password: isPasswordGenerated ? generatedPassword : password,
confirmPassword: isPasswordGenerated
? generatedPassword
: confirmPassword,
createPasswordType: CreatePasswordType.Admincreate,
}),
};
onSave(userProfile);
};
@ -691,14 +692,26 @@ const CreateUser = ({
name="email"
rules={[
{
pattern: validEmailRegEx,
required: true,
type: 'email',
message: jsonData['form-error-messages']['empty-email'],
message: jsonData['form-error-messages']['invalid-email'],
},
{
pattern: validEmailRegEx,
type: 'email',
message: jsonData['form-error-messages']['invalid-email'],
required: true,
validator: async (_, value) => {
if (validEmailRegEx.test(value) && !forceBot) {
const isEmailAlreadyExists = await checkEmailInUse(value);
if (isEmailAlreadyExists) {
return Promise.reject(
jsonData['form-error-messages']['email-is-in-use']
);
}
return Promise.resolve();
}
},
},
]}>
<Input
@ -781,6 +794,8 @@ const CreateUser = ({
<RichTextEditor initialValue={description} ref={markdownRef} />
</Form.Item>
{!forceBot && (
<>
{isAuthProviderBasic && (
<>
<Radio.Group
@ -795,7 +810,8 @@ const CreateUser = ({
</Radio>
</Radio.Group>
{passwordGenerator === CreatePasswordGenerator.CreatePassword ? (
{passwordGenerator ===
CreatePasswordGenerator.CreatePassword ? (
<div className="m-t-sm">
<Form.Item
label="Password"
@ -849,7 +865,7 @@ const CreateUser = ({
required: true,
},
]}>
<Input
<Input.Password
readOnly
addonAfter={
<div className="flex-center w-16">
@ -883,9 +899,6 @@ const CreateUser = ({
)}
</>
)}
{!forceBot && (
<>
<Form.Item label="Teams" name="teams">
<TeamsSelectable onSelectionChange={setSelectedTeams} />
</Form.Item>

View File

@ -63,7 +63,7 @@ describe('Test manage button component', () => {
});
it('Should render delete modal component on click of delete option', async () => {
render(<ManageButton {...mockProps} />);
render(<ManageButton {...mockProps} canDelete />);
const manageButton = await screen.findByTestId('manage-button');

View File

@ -51,3 +51,9 @@ export const NO_PERMISSION_TO_VIEW =
export const GROUP_TEAM_TYPE_CHANGE_MSG =
"The team type 'Group' cannot be changed. Please create a new team with the preferred type.";
export const INGESTION_BOT_CANT_BE_DELETED =
'You can not delete the ingestion bot.';
export const BOT_ACCOUNT_EMAIL_CHANGE_CONFIRMATION =
'Changing account email will update or create a new bot user';

View File

@ -193,6 +193,7 @@ const jsonData = {
'invalid-email': 'Email is invalid.',
'invalid-url': 'Url is invalid.',
'is-required': 'is required',
'email-is-in-use': 'Email is already in use!',
},
label: {
'delete-entity-text':

View File

@ -31,6 +31,26 @@ const mockUserDetail = {
deleted: false,
};
const botData = {
id: '4755f87d-2a53-4376-97e6-fc072f29cf5a',
name: 'ingestion-bot',
fullyQualifiedName: 'ingestion-bot',
displayName: 'ingestion-bot',
botUser: {
id: 'b91d42cb-2a02-4364-ae80-db08b77f1b0c',
type: 'user',
name: 'ingestion-bot',
fullyQualifiedName: 'ingestion-bot',
deleted: false,
href: 'http://localhost:8585/api/v1/users/b91d42cb-2a02-4364-ae80-db08b77f1b0c',
},
version: 0.1,
updatedAt: 1664267598781,
updatedBy: 'ingestion-bot',
href: 'http://localhost:8585/api/v1/bots/4755f87d-2a53-4376-97e6-fc072f29cf5a',
deleted: false,
};
jest.mock('../../components/BotDetails/BotDetails.component', () => {
return jest
.fn()
@ -38,6 +58,7 @@ jest.mock('../../components/BotDetails/BotDetails.component', () => {
});
jest.mock('../../axiosAPIs/userAPI', () => ({
getBotByName: jest.fn().mockImplementation(() => Promise.resolve(botData)),
getUserByName: jest.fn().mockImplementation(() => Promise.resolve()),
revokeUserToken: jest.fn().mockImplementation(() => Promise.resolve()),
updateUserDetail: jest.fn().mockImplementation(() => Promise.resolve()),

View File

@ -17,9 +17,10 @@ import { compare } from 'fast-json-patch';
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import {
getBotByName,
getUserByName,
revokeUserToken,
updateUserDetail,
updateBotDetail,
} from '../../axiosAPIs/userAPI';
import BotDetails from '../../components/BotDetails/BotDetails.component';
import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder';
@ -32,6 +33,7 @@ import {
} from '../../components/PermissionProvider/PermissionProvider.interface';
import { UserDetails } from '../../components/Users/Users.interface';
import { NO_PERMISSION_TO_VIEW } from '../../constants/HelperTextUtil';
import { Bot } from '../../generated/entity/bot';
import { User } from '../../generated/entity/teams/user';
import jsonData from '../../jsons/en';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
@ -40,7 +42,8 @@ import { showErrorToast } from '../../utils/ToastUtils';
const BotDetailsPage = () => {
const { botsName } = useParams<{ [key: string]: string }>();
const { getEntityPermissionByFqn } = usePermissionProvider();
const [botsData, setBotsData] = useState<User>({} as User);
const [botUserData, setBotUserData] = useState<User>({} as User);
const [botData, setBotData] = useState<Bot>({} as Bot);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [botPermission, setBotPermission] = useState<OperationPermission>(
@ -62,34 +65,32 @@ const BotDetailsPage = () => {
}
};
const fetchBotsData = () => {
const fetchBotsData = async () => {
try {
setIsLoading(true);
getUserByName(botsName)
.then((res) => {
if (res) {
setBotsData(res);
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['fetch-user-details-error']
const botResponse = await getBotByName(botsName);
const botUserResponse = await getUserByName(
botResponse.botUser.fullyQualifiedName || ''
);
setBotUserData(botUserResponse);
setBotData(botResponse);
} catch (error) {
showErrorToast(error as AxiosError);
setIsError(true);
})
.finally(() => setIsLoading(false));
} finally {
setIsLoading(false);
}
};
const updateBotsDetails = async (data: UserDetails) => {
const updatedDetails = { ...botsData, ...data };
const jsonPatch = compare(botsData, updatedDetails);
const updatedDetails = { ...botData, ...data };
const jsonPatch = compare(botData, updatedDetails);
try {
const response = await updateUserDetail(botsData.id, jsonPatch);
const response = await updateBotDetail(botData.id, jsonPatch);
if (response) {
setBotsData((prevData) => ({
setBotData((prevData) => ({
...prevData,
...response,
}));
@ -102,10 +103,10 @@ const BotDetailsPage = () => {
};
const revokeBotsToken = () => {
revokeUserToken(botsData.id)
revokeUserToken(botUserData.id)
.then((res) => {
const data = res;
setBotsData(data);
setBotUserData(data);
})
.catch((err: AxiosError) => {
showErrorToast(err);
@ -129,10 +130,12 @@ const BotDetailsPage = () => {
} else {
return (
<BotDetails
botData={botData}
botPermission={botPermission}
botsData={botsData}
botUserData={botUserData}
revokeTokenHandler={revokeBotsToken}
updateBotsDetails={updateBotsDetails}
onEmailChange={fetchBotsData}
/>
);
}

View File

@ -16,9 +16,13 @@ import { observer } from 'mobx-react';
import { LoadingState } from 'Models';
import React, { useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { createBot } from '../../axiosAPIs/botsAPI';
import { createBotWithPut } from '../../axiosAPIs/botsAPI';
import { getRoles } from '../../axiosAPIs/rolesAPIV1';
import { createUser } from '../../axiosAPIs/userAPI';
import {
createUser,
createUserWithPut,
getBotByName,
} from '../../axiosAPIs/userAPI';
import PageContainerV1 from '../../components/containers/PageContainerV1';
import CreateUserComponent from '../../components/CreateUser/CreateUser.component';
import { PAGE_SIZE_LARGE } from '../../constants/constants';
@ -72,56 +76,86 @@ const CreateUserPage = () => {
setStatus('initial');
};
const checkBotInUse = async (name: string) => {
try {
const response = await getBotByName(name);
return Boolean(response);
} catch (_error) {
return false;
}
};
/**
* 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) {
const handleAddUserSave = async (userData: CreateUser) => {
if (bot) {
createBot({
botUser: { id: res.id, type: EntityType.USER },
name: res.name,
displayName: res.displayName,
description: res.description,
} as Bot)
.then((res) => {
const isBotExists = await checkBotInUse(userData.name);
if (isBotExists) {
showErrorToast(`${userData.name} bot already exists.`);
} else {
try {
setStatus('waiting');
// Create a user with isBot:true
const userResponse = await createUserWithPut({
...userData,
botName: userData.name,
});
// Create a bot entity with botUser data
const botResponse = await createBotWithPut({
botUser: { id: userResponse.id, type: EntityType.USER },
name: userResponse.name,
displayName: userResponse.displayName,
description: userResponse.description,
} as Bot);
if (botResponse) {
setStatus('success');
res && showSuccessToast(`Bot created successfully`);
showSuccessToast(`Bot created successfully`);
setTimeout(() => {
setStatus('initial');
goToUserListPage();
}, 500);
})
.catch((err: AxiosError) => {
} else {
handleSaveFailure(
err,
jsonData['api-error-messages']['create-bot-error']
);
});
}
} catch (error) {
handleSaveFailure(
error as AxiosError,
jsonData['api-error-messages']['create-bot-error']
);
}
}
} else {
try {
setStatus('waiting');
const response = await createUser(userData);
if (response) {
setStatus('success');
setTimeout(() => {
setStatus('initial');
goToUserListPage();
}, 500);
}
} else {
handleSaveFailure(
jsonData['api-error-messages']['create-user-error']
);
}
})
.catch((err: AxiosError) => {
} catch (error) {
handleSaveFailure(
err,
error as AxiosError,
jsonData['api-error-messages']['create-user-error']
);
});
}
}
};
const fetchRoles = async () => {

View File

@ -14,8 +14,9 @@
import { isUndefined } from 'lodash';
import moment from 'moment';
import { AuthTypes } from '../enums/signin.enum';
import { AuthenticationMechanism } from '../generated/api/teams/createUser';
import { SsoServiceType } from '../generated/entity/teams/authN/ssoAuth';
import { AuthType, JWTTokenExpiry } from '../generated/entity/teams/user';
import { AuthType, JWTTokenExpiry, User } from '../generated/entity/teams/user';
export const getJWTTokenExpiryOptions = () => {
return Object.keys(JWTTokenExpiry).map((expiry) => {
@ -114,9 +115,34 @@ export const getTokenExpiry = (expiry: number) => {
};
};
export const DEFAULT_GOOGLE_SSO_CLIENT_CONFIG = {
secretKey: '',
audience: 'https://www.googleapis.com/oauth2/v4/token',
};
export const getAuthMechanismFormInitialValues = (
authMechanism: AuthenticationMechanism,
botUser: User
) => {
const authConfig = authMechanism.config?.authConfig;
const email = botUser.email;
export const SECRET_KEY_ERROR_MSG = 'SecretKey is required!';
return {
audience: authConfig?.audience,
secretKey: authConfig?.secretKey,
clientId: authConfig?.clientId,
oktaEmail: authConfig?.email,
orgURL: authConfig?.orgURL,
privateKey: authConfig?.privateKey,
scopes: authConfig?.scopes?.join(','),
domain: authConfig?.domain,
authority: authConfig?.authority,
clientSecret: authConfig?.clientSecret,
tokenEndpoint: authConfig?.tokenEndpoint,
email,
};
};