From c27657ea5e883ceee2a4dc5709136a7262a16bf9 Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Mon, 3 Oct 2022 05:16:19 +0530 Subject: [PATCH] =?UTF-8?q?Feat=20=E2=9C=A8=20#7770=20UI=20:=20Allow=20use?= =?UTF-8?q?r=20to=20edit=20botUser=20Details=20on=20Bot=20profile=20page.?= =?UTF-8?q?=20(#7771)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/cypress/e2e/Pages/Bots.spec.js | 10 +- .../resources/ui/src/axiosAPIs/botsAPI.ts | 9 + .../resources/ui/src/axiosAPIs/userAPI.ts | 32 ++ .../components/BotDetails/AuthMechanism.tsx | 19 +- .../BotDetails/AuthMechanismForm.tsx | 273 +++++++++++++----- .../BotDetails/BotDetails.component.tsx | 58 ++-- .../components/BotDetails/BotDetails.test.tsx | 31 +- .../BotListV1/BotListV1.component.tsx | 71 +++-- .../CreateUser/CreateUser.component.tsx | 239 +++++++-------- .../ManageButton/ManageButton.test.tsx | 2 +- .../ui/src/constants/HelperTextUtil.ts | 6 + .../src/main/resources/ui/src/jsons/en.ts | 1 + .../BotDetailsPage/BotDetailsPage.test.tsx | 21 ++ .../pages/BotDetailsPage/BotDetailsPage.tsx | 57 ++-- .../CreateUserPage.component.tsx | 100 ++++--- .../main/resources/ui/src/utils/BotsUtils.ts | 38 ++- 16 files changed, 650 insertions(+), 317 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.js index 9a93f75b093..884df9af5a1 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Bots.spec.js @@ -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); diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/botsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/botsAPI.ts index fdacfe7dace..398d7c639ca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/botsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/botsAPI.ts @@ -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>( + BASE_URL, + data + ); + + return response.data; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/userAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/userAPI.ts index 6a86f0d81b0..1fe72e53811 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/userAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/userAPI.ts @@ -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(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>( + `/bots/${id}`, + data, + configOptions + ); + + return response.data; +}; + +export const createUserWithPut = async (userDetails: CreateUser) => { + const response = await APIClient.put>( + `/users`, + userDetails + ); + + return response.data; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanism.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanism.tsx index 9fad381bfcc..2770824be81 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanism.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanism.tsx @@ -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 = ({ hasPermission, onEdit, onTokenRevoke, + botUser, }: Props) => { if (authenticationMechanism.authType === AuthType.Jwt) { const JWTToken = authenticationMechanism.config?.JWTToken; @@ -137,6 +142,18 @@ const AuthMechanism: FC = ({ + <> + Account Email + + + + + + {authConfig?.secretKey && ( <> SecretKey diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanismForm.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanismForm.tsx index 056c8adea84..f841751f53c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanismForm.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/AuthMechanismForm.tsx @@ -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 = ({ @@ -47,6 +63,9 @@ const AuthMechanismForm: FC = ({ onSave, onCancel, authenticationMechanism, + botUser, + botData, + onEmailChange, }) => { const { authConfig } = useAuthContext(); @@ -62,6 +81,11 @@ const AuthMechanismForm: FC = ({ ({} as SSOClientConfig) ); + const [accountEmail, setAccountEmail] = useState(botUser.email); + + const [isConfirmationModalOpen, setIsConfirmationModalOpen] = + useState(false); + useEffect(() => { const authType = authenticationMechanism.authType; const authConfig = authenticationMechanism.config?.authConfig; @@ -114,6 +138,10 @@ const AuthMechanismForm: FC = ({ email: value, })); + break; + case 'email': + setAccountEmail(value); + break; default: @@ -122,22 +150,80 @@ const AuthMechanismForm: FC = ({ }; const handleSave = () => { - const updatedAuthMechanism: AuthenticationMechanism = { - authType: authMechanism, - config: - authMechanism === AuthType.Jwt - ? { - JWTTokenExpiry: tokenExpiry, - } - : { - ssoServiceType: authConfig?.provider as SsoServiceType, - authConfig: { - ...ssoClientConfig, + if (accountEmail !== botUser.email) { + setIsConfirmationModalOpen(true); + } else { + const updatedAuthMechanism: AuthenticationMechanism = { + authType: authMechanism, + config: + authMechanism === AuthType.Jwt + ? { + JWTTokenExpiry: tokenExpiry, + } + : { + ssoServiceType: authConfig?.provider as SsoServiceType, + authConfig: { + ...ssoClientConfig, + }, }, - }, - }; + }; - 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 = () => { @@ -464,47 +550,24 @@ const AuthMechanismForm: FC = ({ }; return ( -
- { - if (!authMechanism) { - return Promise.reject('Auth Mechanism is required'); - } - - return Promise.resolve(); - }, - }, - ]}> - - - - {authMechanism === AuthType.Jwt && ( + <> + { - if (!tokenExpiry) { - return Promise.reject('Token Expiration is required'); + if (!authMechanism) { + return Promise.reject('Auth Mechanism is required'); } return Promise.resolve(); @@ -513,32 +576,98 @@ const AuthMechanismForm: FC = ({ ]}> - )} - {authMechanism === AuthType.Sso && <>{getSSOConfig()}} - - {!isEmpty(authenticationMechanism) && ( - + + {authMechanism === AuthType.Jwt && ( + { + if (!tokenExpiry) { + return Promise.reject('Token Expiration is required'); + } + + return Promise.resolve(); + }, + }, + ]}> + + )} - - -
+ {authMechanism === AuthType.Sso && ( + <> + + + + {getSSOConfig()} + + )} + + {!isEmpty(authenticationMechanism) && ( + + )} + + + + {isConfirmationModalOpen && ( + setIsConfirmationModalOpen(false)} + onOk={handleAccountEmailChange}> + + {BOT_ACCOUNT_EMAIL_CHANGE_CONFIRMATION} for {botData.name} bot. + + + )} + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.component.tsx index 7be6ade5537..1690b393ee8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.component.tsx @@ -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 { - botsData: User; + botUserData: User; + botData: Bot; botPermission: OperationPermission; updateBotsDetails: (data: UserDetails) => Promise; revokeTokenHandler: () => void; + onEmailChange: () => void; } const BotDetails: FC = ({ - 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(false); @@ -92,7 +98,7 @@ const BotDetails: FC = ({ 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 = ({ try { const { isAdmin, - teams, timezone, name, description, @@ -114,11 +119,9 @@ const BotDetails: FC = ({ 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 = ({ profile, email, isBot, - roles, authenticationMechanism: { - ...botsData.authenticationMechanism, + ...botUserData.authenticationMechanism, authType: updatedAuthMechanism.authType, config: updatedAuthMechanism.authType === AuthType.Jwt @@ -140,8 +142,15 @@ const BotDetails: FC = ({ 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 = ({ }; const handleDisplayNameChange = () => { - if (displayName !== botsData.displayName) { + if (displayName !== botData.displayName) { updateBotsDetails({ displayName: displayName || '' }); } setIsDisplayNameEdit(false); @@ -243,8 +252,8 @@ const BotDetails: FC = ({ return (
setIsDescriptionEdit(false)} @@ -303,10 +312,10 @@ const BotDetails: FC = ({ ); useEffect(() => { - if (botsData.id) { + if (botUserData.id) { fetchAuthMechanismForBot(); } - }, [botsData]); + }, [botUserData]); return ( = ({ GlobalSettingOptions.BOTS ), }, - { name: botsData.name || '', url: '', activeTitle: true }, + { name: botData.name || '', url: '', activeTitle: true }, ]} /> } @@ -339,13 +348,17 @@ const BotDetails: FC = ({ {isAuthMechanismEdit ? ( setIsAuthMechanismEdit(false)} + onEmailChange={onEmailChange} onSave={handleAuthMechanismUpdate} /> ) : ( setIsRevokingToken(true)} @@ -355,8 +368,11 @@ const BotDetails: FC = ({ ) : ( setIsAuthMechanismEdit(false)} + onEmailChange={onEmailChange} onSave={handleAuthMechanismUpdate} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.test.tsx index 47beb40a35d..4ed61882eb0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BotDetails/BotDetails.test.tsx @@ -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)), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BotListV1/BotListV1.component.tsx index 208476eb9b9..29e50842008 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BotListV1/BotListV1.component.tsx @@ -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) => ( - {getEntityName(record?.botUser)} + data-testid={`bot-link-${getEntityName(record)}`} + to={getBotsPath(record?.fullyQualifiedName || record?.name || '')}> + {getEntityName(record)} ), }, @@ -127,10 +128,8 @@ const BotListV1 = ({ dataIndex: 'description', key: 'description', render: (_, record) => - record?.botUser?.description ? ( - + record?.description ? ( + ) : ( No Description ), @@ -140,27 +139,35 @@ const BotListV1 = ({ dataIndex: 'id', key: 'id', width: 90, - render: (_, record) => ( - - -
- )} - - )} - {!forceBot && ( <> + {isAuthProviderBasic && ( + <> + + + Automatic Generate + + + Create Password + + + + {passwordGenerator === + CreatePasswordGenerator.CreatePassword ? ( +
+ + + + + { + if (value !== password) { + return Promise.reject("Password doesn't match"); + } + + return Promise.resolve(); + }, + }, + ]}> + + +
+ ) : ( +
+ + +
+ {isPasswordGenerating ? ( + + ) : ( + + )} +
+ +
+ +
+
+ } + name="generatedPassword" + value={generatedPassword} + /> + + + )} + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/ManageButton/ManageButton.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/ManageButton/ManageButton.test.tsx index df1f1e0138a..c92f58540ee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/ManageButton/ManageButton.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/ManageButton/ManageButton.test.tsx @@ -63,7 +63,7 @@ describe('Test manage button component', () => { }); it('Should render delete modal component on click of delete option', async () => { - render(); + render(); const manageButton = await screen.findByTestId('manage-button'); diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/HelperTextUtil.ts b/openmetadata-ui/src/main/resources/ui/src/constants/HelperTextUtil.ts index 5ca27a1969e..57dc1f7c3f9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/HelperTextUtil.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/HelperTextUtil.ts @@ -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'; diff --git a/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts b/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts index dd522b0d5d1..5d6f69a17c8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts +++ b/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts @@ -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': diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/BotDetailsPage/BotDetailsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/BotDetailsPage/BotDetailsPage.test.tsx index 39edef7e54b..2572756426c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/BotDetailsPage/BotDetailsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/BotDetailsPage/BotDetailsPage.test.tsx @@ -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()), diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/BotDetailsPage/BotDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/BotDetailsPage/BotDetailsPage.tsx index 97fe738fd71..ab13a288af1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/BotDetailsPage/BotDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/BotDetailsPage/BotDetailsPage.tsx @@ -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({} as User); + const [botUserData, setBotUserData] = useState({} as User); + const [botData, setBotData] = useState({} as Bot); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const [botPermission, setBotPermission] = useState( @@ -62,34 +65,32 @@ const BotDetailsPage = () => { } }; - const fetchBotsData = () => { - 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'] - ); - setIsError(true); - }) - .finally(() => setIsLoading(false)); + const fetchBotsData = async () => { + try { + setIsLoading(true); + 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); + } }; 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 ( ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CreateUserPage/CreateUserPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CreateUserPage/CreateUserPage.component.tsx index e488485d536..fcc3139598d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CreateUserPage/CreateUserPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CreateUserPage/CreateUserPage.component.tsx @@ -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) { - if (bot) { - createBot({ - botUser: { id: res.id, type: EntityType.USER }, - name: res.name, - displayName: res.displayName, - description: res.description, - } as Bot) - .then((res) => { - setStatus('success'); - res && showSuccessToast(`Bot created successfully`); - setTimeout(() => { - setStatus('initial'); + const handleAddUserSave = async (userData: CreateUser) => { + if (bot) { + 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, + }); - goToUserListPage(); - }, 500); - }) - .catch((err: AxiosError) => { - handleSaveFailure( - err, - jsonData['api-error-messages']['create-bot-error'] - ); - }); - } else { + // 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'); + showSuccessToast(`Bot created successfully`); setTimeout(() => { setStatus('initial'); + goToUserListPage(); }, 500); + } else { + handleSaveFailure( + 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 () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.ts index f89cb617555..72a84386d47 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.ts @@ -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, + }; +};