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

View File

@ -63,3 +63,12 @@ export const deleteBot = async (id: string, hardDelete?: boolean) => {
return response.data; 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, AuthenticationMechanism,
CreateUser, CreateUser,
} from '../generated/api/teams/createUser'; } from '../generated/api/teams/createUser';
import { Bot } from '../generated/entity/bot';
import { JwtAuth } from '../generated/entity/teams/authN/jwtAuth'; import { JwtAuth } from '../generated/entity/teams/authN/jwtAuth';
import { User } from '../generated/entity/teams/user'; import { User } from '../generated/entity/teams/user';
import { EntityReference } from '../generated/type/entityReference'; import { EntityReference } from '../generated/type/entityReference';
@ -196,3 +197,34 @@ export const getAuthMechanismForBotUser = async (botId: string) => {
return response.data; 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 { capitalize } from 'lodash';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { AuthType } from '../../generated/api/teams/createUser'; 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 { getTokenExpiry } from '../../utils/BotsUtils';
import SVGIcons from '../../utils/SvgUtils'; import SVGIcons from '../../utils/SvgUtils';
import CopyToClipboardButton from '../buttons/CopyToClipboardButton/CopyToClipboardButton'; import CopyToClipboardButton from '../buttons/CopyToClipboardButton/CopyToClipboardButton';
import './AuthMechanism.less'; import './AuthMechanism.less';
interface Props { interface Props {
botUser: User;
authenticationMechanism: AuthenticationMechanism; authenticationMechanism: AuthenticationMechanism;
hasPermission: boolean; hasPermission: boolean;
onEdit: () => void; onEdit: () => void;
@ -33,6 +37,7 @@ const AuthMechanism: FC<Props> = ({
hasPermission, hasPermission,
onEdit, onEdit,
onTokenRevoke, onTokenRevoke,
botUser,
}: Props) => { }: Props) => {
if (authenticationMechanism.authType === AuthType.Jwt) { if (authenticationMechanism.authType === AuthType.Jwt) {
const JWTToken = authenticationMechanism.config?.JWTToken; const JWTToken = authenticationMechanism.config?.JWTToken;
@ -137,6 +142,18 @@ const AuthMechanism: FC<Props> = ({
<Divider style={{ margin: '8px 0px' }} /> <Divider style={{ margin: '8px 0px' }} />
<Space className="w-full" direction="vertical"> <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 && ( {authConfig?.secretKey && (
<> <>
<Typography.Text>SecretKey</Typography.Text> <Typography.Text>SecretKey</Typography.Text>

View File

@ -11,35 +11,51 @@
* limitations under the License. * 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 { isEmpty } from 'lodash';
import React, { FC, useEffect, useState } from 'react'; import React, { FC, useEffect, useState } from 'react';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider'; 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 { SsoServiceType } from '../../generated/entity/teams/authN/ssoAuth';
import { import {
AuthenticationMechanism, AuthenticationMechanism,
AuthType, AuthType,
JWTTokenExpiry, JWTTokenExpiry,
User,
} from '../../generated/entity/teams/user'; } from '../../generated/entity/teams/user';
import { Auth0SSOClientConfig } from '../../generated/security/client/auth0SSOClientConfig'; import { Auth0SSOClientConfig } from '../../generated/security/client/auth0SSOClientConfig';
import { AzureSSOClientConfig } from '../../generated/security/client/azureSSOClientConfig'; import { AzureSSOClientConfig } from '../../generated/security/client/azureSSOClientConfig';
import { CustomOidcSSOClientConfig } from '../../generated/security/client/customOidcSSOClientConfig'; import { CustomOidcSSOClientConfig } from '../../generated/security/client/customOidcSSOClientConfig';
import { GoogleSSOClientConfig } from '../../generated/security/client/googleSSOClientConfig'; import { GoogleSSOClientConfig } from '../../generated/security/client/googleSSOClientConfig';
import { OktaSSOClientConfig } from '../../generated/security/client/oktaSSOClientConfig'; import { OktaSSOClientConfig } from '../../generated/security/client/oktaSSOClientConfig';
import jsonData from '../../jsons/en';
import { getNameFromEmail } from '../../utils/AuthProvider.util';
import { import {
getAuthMechanismFormInitialValues,
getAuthMechanismTypeOptions, getAuthMechanismTypeOptions,
getJWTTokenExpiryOptions, getJWTTokenExpiryOptions,
} from '../../utils/BotsUtils'; } from '../../utils/BotsUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { SSOClientConfig } from '../CreateUser/CreateUser.interface'; import { SSOClientConfig } from '../CreateUser/CreateUser.interface';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
const { Option } = Select; const { Option } = Select;
interface Props { interface Props {
botUser: User;
botData: Bot;
isUpdating: boolean; isUpdating: boolean;
authenticationMechanism: AuthenticationMechanism; authenticationMechanism: AuthenticationMechanism;
onSave: (updatedAuthMechanism: AuthenticationMechanism) => void; onSave: (updatedAuthMechanism: AuthenticationMechanism) => void;
onCancel: () => void; onCancel: () => void;
onEmailChange: () => void;
} }
const AuthMechanismForm: FC<Props> = ({ const AuthMechanismForm: FC<Props> = ({
@ -47,6 +63,9 @@ const AuthMechanismForm: FC<Props> = ({
onSave, onSave,
onCancel, onCancel,
authenticationMechanism, authenticationMechanism,
botUser,
botData,
onEmailChange,
}) => { }) => {
const { authConfig } = useAuthContext(); const { authConfig } = useAuthContext();
@ -62,6 +81,11 @@ const AuthMechanismForm: FC<Props> = ({
({} as SSOClientConfig) ({} as SSOClientConfig)
); );
const [accountEmail, setAccountEmail] = useState<string>(botUser.email);
const [isConfirmationModalOpen, setIsConfirmationModalOpen] =
useState<boolean>(false);
useEffect(() => { useEffect(() => {
const authType = authenticationMechanism.authType; const authType = authenticationMechanism.authType;
const authConfig = authenticationMechanism.config?.authConfig; const authConfig = authenticationMechanism.config?.authConfig;
@ -114,6 +138,10 @@ const AuthMechanismForm: FC<Props> = ({
email: value, email: value,
})); }));
break;
case 'email':
setAccountEmail(value);
break; break;
default: default:
@ -122,6 +150,9 @@ const AuthMechanismForm: FC<Props> = ({
}; };
const handleSave = () => { const handleSave = () => {
if (accountEmail !== botUser.email) {
setIsConfirmationModalOpen(true);
} else {
const updatedAuthMechanism: AuthenticationMechanism = { const updatedAuthMechanism: AuthenticationMechanism = {
authType: authMechanism, authType: authMechanism,
config: config:
@ -138,6 +169,61 @@ const AuthMechanismForm: FC<Props> = ({
}; };
onSave(updatedAuthMechanism); 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 = () => { const getSSOConfig = () => {
@ -464,8 +550,13 @@ const AuthMechanismForm: FC<Props> = ({
}; };
return ( return (
<>
<Form <Form
id="update-auth-mechanism-form" id="update-auth-mechanism-form"
initialValues={getAuthMechanismFormInitialValues(
authenticationMechanism,
botUser
)}
layout="vertical" layout="vertical"
onFinish={handleSave}> onFinish={handleSave}>
<Form.Item <Form.Item
@ -523,7 +614,30 @@ const AuthMechanismForm: FC<Props> = ({
</Select> </Select>
</Form.Item> </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}> <Space className="w-full tw-justify-end" size={4}>
{!isEmpty(authenticationMechanism) && ( {!isEmpty(authenticationMechanism) && (
<Button data-testid="cancel-edit" type="link" onClick={onCancel}> <Button data-testid="cancel-edit" type="link" onClick={onCancel}>
@ -539,6 +653,21 @@ const AuthMechanismForm: FC<Props> = ({
</Button> </Button>
</Space> </Space>
</Form> </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, useMemo,
useState, useState,
} from 'react'; } from 'react';
import { createBotWithPut } from '../../axiosAPIs/botsAPI';
import { import {
createUserWithPut,
getAuthMechanismForBotUser, getAuthMechanismForBotUser,
updateUser,
} from '../../axiosAPIs/userAPI'; } from '../../axiosAPIs/userAPI';
import { import {
GlobalSettingOptions, GlobalSettingOptions,
GlobalSettingsMenuCategory, GlobalSettingsMenuCategory,
} from '../../constants/globalSettings.constants'; } from '../../constants/globalSettings.constants';
import { EntityType } from '../../enums/entity.enum';
import { Bot } from '../../generated/entity/bot';
import { import {
AuthenticationMechanism, AuthenticationMechanism,
AuthType, AuthType,
User, User,
} from '../../generated/entity/teams/user'; } from '../../generated/entity/teams/user';
import { EntityReference } from '../../generated/type/entityReference';
import { getEntityName } from '../../utils/CommonUtils'; import { getEntityName } from '../../utils/CommonUtils';
import { getSettingPath } from '../../utils/RouterUtils'; import { getSettingPath } from '../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils'; import SVGIcons, { Icons } from '../../utils/SvgUtils';
@ -51,19 +53,23 @@ import AuthMechanism from './AuthMechanism';
import AuthMechanismForm from './AuthMechanismForm'; import AuthMechanismForm from './AuthMechanismForm';
interface BotsDetailProp extends HTMLAttributes<HTMLDivElement> { interface BotsDetailProp extends HTMLAttributes<HTMLDivElement> {
botsData: User; botUserData: User;
botData: Bot;
botPermission: OperationPermission; botPermission: OperationPermission;
updateBotsDetails: (data: UserDetails) => Promise<void>; updateBotsDetails: (data: UserDetails) => Promise<void>;
revokeTokenHandler: () => void; revokeTokenHandler: () => void;
onEmailChange: () => void;
} }
const BotDetails: FC<BotsDetailProp> = ({ const BotDetails: FC<BotsDetailProp> = ({
botsData, botData,
botUserData,
updateBotsDetails, updateBotsDetails,
revokeTokenHandler, revokeTokenHandler,
botPermission, botPermission,
onEmailChange,
}) => { }) => {
const [displayName, setDisplayName] = useState(botsData.displayName); const [displayName, setDisplayName] = useState(botData.displayName);
const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false); const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false);
const [isDescriptionEdit, setIsDescriptionEdit] = useState(false); const [isDescriptionEdit, setIsDescriptionEdit] = useState(false);
const [isRevokingToken, setIsRevokingToken] = useState<boolean>(false); const [isRevokingToken, setIsRevokingToken] = useState<boolean>(false);
@ -92,7 +98,7 @@ const BotDetails: FC<BotsDetailProp> = ({
const fetchAuthMechanismForBot = async () => { const fetchAuthMechanismForBot = async () => {
try { try {
const response = await getAuthMechanismForBotUser(botsData.id); const response = await getAuthMechanismForBotUser(botUserData.id);
setAuthenticationMechanism(response); setAuthenticationMechanism(response);
} catch (error) { } catch (error) {
showErrorToast(error as AxiosError); showErrorToast(error as AxiosError);
@ -106,7 +112,6 @@ const BotDetails: FC<BotsDetailProp> = ({
try { try {
const { const {
isAdmin, isAdmin,
teams,
timezone, timezone,
name, name,
description, description,
@ -114,11 +119,9 @@ const BotDetails: FC<BotsDetailProp> = ({
profile, profile,
email, email,
isBot, isBot,
roles, } = botUserData;
} = botsData; const response = await createUserWithPut({
const response = await updateUser({
isAdmin, isAdmin,
teams,
timezone, timezone,
name, name,
description, description,
@ -126,9 +129,8 @@ const BotDetails: FC<BotsDetailProp> = ({
profile, profile,
email, email,
isBot, isBot,
roles,
authenticationMechanism: { authenticationMechanism: {
...botsData.authenticationMechanism, ...botUserData.authenticationMechanism,
authType: updatedAuthMechanism.authType, authType: updatedAuthMechanism.authType,
config: config:
updatedAuthMechanism.authType === AuthType.Jwt updatedAuthMechanism.authType === AuthType.Jwt
@ -140,8 +142,15 @@ const BotDetails: FC<BotsDetailProp> = ({
authConfig: updatedAuthMechanism.config?.authConfig, authConfig: updatedAuthMechanism.config?.authConfig,
}, },
}, },
} as User); botName: botData.name,
if (response.data) { });
if (response) {
await createBotWithPut({
name: botData.name,
description: botData.description,
displayName: botData.displayName,
botUser: { id: response.id, type: EntityType.USER },
});
fetchAuthMechanismForBot(); fetchAuthMechanismForBot();
} }
} catch (error) { } catch (error) {
@ -159,7 +168,7 @@ const BotDetails: FC<BotsDetailProp> = ({
}; };
const handleDisplayNameChange = () => { const handleDisplayNameChange = () => {
if (displayName !== botsData.displayName) { if (displayName !== botData.displayName) {
updateBotsDetails({ displayName: displayName || '' }); updateBotsDetails({ displayName: displayName || '' });
} }
setIsDisplayNameEdit(false); setIsDisplayNameEdit(false);
@ -243,8 +252,8 @@ const BotDetails: FC<BotsDetailProp> = ({
return ( return (
<div className="tw--ml-5"> <div className="tw--ml-5">
<Description <Description
description={botsData.description || ''} description={botData.description || ''}
entityName={getEntityName(botsData as unknown as EntityReference)} entityName={getEntityName(botData)}
hasEditAccess={descriptionPermission || editAllPermission} hasEditAccess={descriptionPermission || editAllPermission}
isEdit={isDescriptionEdit} isEdit={isDescriptionEdit}
onCancel={() => setIsDescriptionEdit(false)} onCancel={() => setIsDescriptionEdit(false)}
@ -303,10 +312,10 @@ const BotDetails: FC<BotsDetailProp> = ({
); );
useEffect(() => { useEffect(() => {
if (botsData.id) { if (botUserData.id) {
fetchAuthMechanismForBot(); fetchAuthMechanismForBot();
} }
}, [botsData]); }, [botUserData]);
return ( return (
<PageLayout <PageLayout
@ -322,7 +331,7 @@ const BotDetails: FC<BotsDetailProp> = ({
GlobalSettingOptions.BOTS GlobalSettingOptions.BOTS
), ),
}, },
{ name: botsData.name || '', url: '', activeTitle: true }, { name: botData.name || '', url: '', activeTitle: true },
]} ]}
/> />
} }
@ -339,13 +348,17 @@ const BotDetails: FC<BotsDetailProp> = ({
{isAuthMechanismEdit ? ( {isAuthMechanismEdit ? (
<AuthMechanismForm <AuthMechanismForm
authenticationMechanism={authenticationMechanism} authenticationMechanism={authenticationMechanism}
botData={botData}
botUser={botUserData}
isUpdating={isUpdating} isUpdating={isUpdating}
onCancel={() => setIsAuthMechanismEdit(false)} onCancel={() => setIsAuthMechanismEdit(false)}
onEmailChange={onEmailChange}
onSave={handleAuthMechanismUpdate} onSave={handleAuthMechanismUpdate}
/> />
) : ( ) : (
<AuthMechanism <AuthMechanism
authenticationMechanism={authenticationMechanism} authenticationMechanism={authenticationMechanism}
botUser={botUserData}
hasPermission={editAllPermission} hasPermission={editAllPermission}
onEdit={handleAuthMechanismEdit} onEdit={handleAuthMechanismEdit}
onTokenRevoke={() => setIsRevokingToken(true)} onTokenRevoke={() => setIsRevokingToken(true)}
@ -355,8 +368,11 @@ const BotDetails: FC<BotsDetailProp> = ({
) : ( ) : (
<AuthMechanismForm <AuthMechanismForm
authenticationMechanism={{} as AuthenticationMechanism} authenticationMechanism={{} as AuthenticationMechanism}
botData={botData}
botUser={botUserData}
isUpdating={isUpdating} isUpdating={isUpdating}
onCancel={() => setIsAuthMechanismEdit(false)} onCancel={() => setIsAuthMechanismEdit(false)}
onEmailChange={onEmailChange}
onSave={handleAuthMechanismUpdate} onSave={handleAuthMechanismUpdate}
/> />
)} )}

View File

@ -20,8 +20,9 @@ import BotDetails from './BotDetails.component';
const revokeTokenHandler = jest.fn(); const revokeTokenHandler = jest.fn();
const updateBotsDetails = jest.fn(); const updateBotsDetails = jest.fn();
const onEmailChange = jest.fn();
const botsData = { const botUserData = {
id: 'ea09aed1-0251-4a75-b92a-b65641610c53', id: 'ea09aed1-0251-4a75-b92a-b65641610c53',
name: 'sachinchaurasiyachotey87', name: 'sachinchaurasiyachotey87',
fullyQualifiedName: 'sachinchaurasiyachotey87', fullyQualifiedName: 'sachinchaurasiyachotey87',
@ -36,6 +37,26 @@ const botsData = {
deleted: false, 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 = { const mockAuthMechanism = {
config: { config: {
JWTToken: JWTToken:
@ -48,7 +69,8 @@ const mockAuthMechanism = {
}; };
const mockProp = { const mockProp = {
botsData, botUserData,
botData,
botPermission: { botPermission: {
Create: true, Create: true,
Delete: true, Delete: true,
@ -60,6 +82,7 @@ const mockProp = {
} as OperationPermission, } as OperationPermission,
revokeTokenHandler, revokeTokenHandler,
updateBotsDetails, updateBotsDetails,
onEmailChange,
}; };
jest.mock('../../utils/PermissionsUtils', () => ({ jest.mock('../../utils/PermissionsUtils', () => ({
@ -68,7 +91,9 @@ jest.mock('../../utils/PermissionsUtils', () => ({
jest.mock('../../axiosAPIs/userAPI', () => { jest.mock('../../axiosAPIs/userAPI', () => {
return { return {
updateUser: jest.fn().mockImplementation(() => Promise.resolve(botsData)), createUserWithPut: jest
.fn()
.mockImplementation(() => Promise.resolve(botUserData)),
getAuthMechanismForBotUser: jest getAuthMechanismForBotUser: jest
.fn() .fn()
.mockImplementation(() => Promise.resolve(mockAuthMechanism)), .mockImplementation(() => Promise.resolve(mockAuthMechanism)),

View File

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

View File

@ -28,7 +28,7 @@ import { isUndefined } from 'lodash';
import { EditorContentRef } from 'Models'; import { EditorContentRef } from 'Models';
import React, { useMemo, useRef, useState } from 'react'; import React, { useMemo, useRef, useState } from 'react';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider'; 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 { getBotsPagePath, getUsersPagePath } from '../../constants/constants';
import { passwordErrorMessage } from '../../constants/error-message'; import { passwordErrorMessage } from '../../constants/error-message';
import { import {
@ -321,11 +321,6 @@ const CreateUser = ({
email: email, email: email,
isAdmin: isAdmin, isAdmin: isAdmin,
isBot: isBot, isBot: isBot,
password: isPasswordGenerated ? generatedPassword : password,
confirmPassword: isPasswordGenerated
? generatedPassword
: confirmPassword,
createPasswordType: CreatePasswordType.Admincreate,
...(forceBot ...(forceBot
? { ? {
authenticationMechanism: { authenticationMechanism: {
@ -343,7 +338,13 @@ const CreateUser = ({
}, },
}, },
} }
: {}), : {
password: isPasswordGenerated ? generatedPassword : password,
confirmPassword: isPasswordGenerated
? generatedPassword
: confirmPassword,
createPasswordType: CreatePasswordType.Admincreate,
}),
}; };
onSave(userProfile); onSave(userProfile);
}; };
@ -691,14 +692,26 @@ const CreateUser = ({
name="email" name="email"
rules={[ rules={[
{ {
pattern: validEmailRegEx,
required: true, required: true,
type: 'email', type: 'email',
message: jsonData['form-error-messages']['empty-email'], message: jsonData['form-error-messages']['invalid-email'],
}, },
{ {
pattern: validEmailRegEx,
type: 'email', 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 <Input
@ -781,6 +794,8 @@ const CreateUser = ({
<RichTextEditor initialValue={description} ref={markdownRef} /> <RichTextEditor initialValue={description} ref={markdownRef} />
</Form.Item> </Form.Item>
{!forceBot && (
<>
{isAuthProviderBasic && ( {isAuthProviderBasic && (
<> <>
<Radio.Group <Radio.Group
@ -795,7 +810,8 @@ const CreateUser = ({
</Radio> </Radio>
</Radio.Group> </Radio.Group>
{passwordGenerator === CreatePasswordGenerator.CreatePassword ? ( {passwordGenerator ===
CreatePasswordGenerator.CreatePassword ? (
<div className="m-t-sm"> <div className="m-t-sm">
<Form.Item <Form.Item
label="Password" label="Password"
@ -849,7 +865,7 @@ const CreateUser = ({
required: true, required: true,
}, },
]}> ]}>
<Input <Input.Password
readOnly readOnly
addonAfter={ addonAfter={
<div className="flex-center w-16"> <div className="flex-center w-16">
@ -883,9 +899,6 @@ const CreateUser = ({
)} )}
</> </>
)} )}
{!forceBot && (
<>
<Form.Item label="Teams" name="teams"> <Form.Item label="Teams" name="teams">
<TeamsSelectable onSelectionChange={setSelectedTeams} /> <TeamsSelectable onSelectionChange={setSelectedTeams} />
</Form.Item> </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 () => { 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'); 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 = export const GROUP_TEAM_TYPE_CHANGE_MSG =
"The team type 'Group' cannot be changed. Please create a new team with the preferred type."; "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-email': 'Email is invalid.',
'invalid-url': 'Url is invalid.', 'invalid-url': 'Url is invalid.',
'is-required': 'is required', 'is-required': 'is required',
'email-is-in-use': 'Email is already in use!',
}, },
label: { label: {
'delete-entity-text': 'delete-entity-text':

View File

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

View File

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

View File

@ -14,8 +14,9 @@
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { AuthTypes } from '../enums/signin.enum'; import { AuthTypes } from '../enums/signin.enum';
import { AuthenticationMechanism } from '../generated/api/teams/createUser';
import { SsoServiceType } from '../generated/entity/teams/authN/ssoAuth'; 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 = () => { export const getJWTTokenExpiryOptions = () => {
return Object.keys(JWTTokenExpiry).map((expiry) => { return Object.keys(JWTTokenExpiry).map((expiry) => {
@ -114,9 +115,34 @@ export const getTokenExpiry = (expiry: number) => {
}; };
}; };
export const DEFAULT_GOOGLE_SSO_CLIENT_CONFIG = { export const getAuthMechanismFormInitialValues = (
secretKey: '', authMechanism: AuthenticationMechanism,
audience: 'https://www.googleapis.com/oauth2/v4/token', 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,
};
};