diff --git a/openmetadata-ui/src/main/resources/ui/jest.config.js b/openmetadata-ui/src/main/resources/ui/jest.config.js index ff1180080b9..273b32a536a 100644 --- a/openmetadata-ui/src/main/resources/ui/jest.config.js +++ b/openmetadata-ui/src/main/resources/ui/jest.config.js @@ -49,5 +49,7 @@ module.exports = { '\\.(scss)$': 'identity-obj-proxy', // Mock style imports '\\.(jpg|JPG|gif|GIF|png|PNG|less|LESS|css|CSS)$': '/src/test/unit/mocks/file.mock.js', + '@fortawesome/react-fontawesome': + '/src/test/unit/mocks/fontawesome.mock.js', }, }; 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 06254602a7b..b7207d92214 100644 --- a/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts +++ b/openmetadata-ui/src/main/resources/ui/src/jsons/en.ts @@ -24,6 +24,7 @@ const jsonData = { 'create-user-error': 'Error while creating user!', 'create-conversation-error': 'Error while creating conversation!', 'create-message-error': 'Error while creating message!', + 'create-team-error': 'Error while creating team!', 'delete-glossary-error': 'Error while deleting glossary!', 'delete-glossary-term-error': 'Error while deleting glossary term!', diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/teams/index.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/teams/index.test.tsx index 16d98a41e4e..1b7ca7dc0ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/teams/index.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/teams/index.test.tsx @@ -18,6 +18,8 @@ import { render, } from '@testing-library/react'; import React, { ReactNode } from 'react'; +import { createTeam, getTeamByName, getTeams } from '../../axiosAPIs/teamsAPI'; +import jsonData from '../../jsons/en'; import TeamsPage from './index'; jest.mock('../../auth-provider/AuthProvider', () => { @@ -320,3 +322,57 @@ describe('Test Teams page', () => { expect(await findByText(container, /ManageTab/i)).toBeInTheDocument(); }); }); + +describe('Test Teams page sad path', () => { + it('Should render error placeholder if getTeamByName api fails', async () => { + (getTeamByName as jest.Mock).mockImplementationOnce(() => + Promise.reject({ + response: { + data: { + message: + jsonData['api-error-messages']['unexpected-server-response'], + }, + }, + }) + ); + const { container } = render(); + + const errorPlaceHolder = await findByTestId(container, 'error'); + + expect(errorPlaceHolder).toBeInTheDocument(); + }); + + it('Should render error placeholder if getTeams api fails', async () => { + (getTeams as jest.Mock).mockImplementationOnce(() => + Promise.reject({ + response: { + data: { + message: jsonData['api-error-messages']['fetch-teams-error'], + }, + }, + }) + ); + const { container } = render(); + + const errorPlaceHolder = await findByTestId(container, 'error'); + + expect(errorPlaceHolder).toBeInTheDocument(); + }); + + it('Should render component if createTeam api fails', async () => { + (createTeam as jest.Mock).mockImplementationOnce(() => + Promise.reject({ + response: { + data: { + message: jsonData['api-error-messages']['fetch-teams-error'], + }, + }, + }) + ); + const { container } = render(); + + const teamComponent = await findByTestId(container, 'team-container'); + + expect(teamComponent).toBeInTheDocument(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/teams/index.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/teams/index.tsx index 1b417c27227..5874a2b6f14 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/teams/index.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/teams/index.tsx @@ -40,7 +40,6 @@ import ManageTabComponent from '../../components/ManageTab/ManageTab.component'; import ConfirmationModal from '../../components/Modals/ConfirmationModal/ConfirmationModal'; import FormModal from '../../components/Modals/FormModal'; import { - ERROR404, getTeamDetailsPath, TITLE_FOR_NON_ADMIN_ACTION, } from '../../constants/constants'; @@ -61,6 +60,7 @@ import { isUrlFriendlyName, } from '../../utils/CommonUtils'; import { getInfoElements } from '../../utils/EntityUtils'; +import { getErrorText } from '../../utils/StringsUtils'; import AddUsersModal from './AddUsersModal'; import Form from './Form'; import UserCard from './UserCard'; @@ -91,6 +91,10 @@ const TeamsPage = () => { const showToast = useToastContext(); + /** + * Take error message as input and display toast message + * @param errMessage - error message + */ const handleShowErrorToast = (errMessage: string) => { showToast({ variant: 'error', @@ -114,6 +118,10 @@ const TeamsPage = () => { }, ]; + /** + * Check if current team is the owner or not + * @returns - True true or false based on hasEditAccess response + */ const isOwner = () => { return hasEditAccess( currentTeam?.owner?.type || '', @@ -121,20 +129,32 @@ const TeamsPage = () => { ); }; + /** + * Make API call to fetch all the teams + */ const fetchTeams = () => { setIsLoading(true); getTeams(['users', 'owns', 'defaultRoles', 'owner']) .then((res: AxiosResponse) => { - if (!team) { - setCurrentTeam(res.data.data[0]); + if (res.data) { + if (!team) { + setCurrentTeam(res.data.data[0]); + } + setTeams(res.data.data); + AppState.updateUserTeam(res.data.data); + } else { + throw jsonData['api-error-messages']['unexpected-server-response']; } - setTeams(res.data.data); - AppState.updateUserTeam(res.data.data); }) .catch((err: AxiosError) => { - if (err?.response?.data.code) { - setError(ERROR404); - } + const errMsg = getErrorText( + err, + jsonData['api-error-messages']['fetch-teams-error'] + ); + + setError(errMsg); + + handleShowErrorToast(errMsg); }) .finally(() => { setIsLoading(false); @@ -150,20 +170,32 @@ const TeamsPage = () => { } }; + /** + * Make API call to fetch current team data + */ const fetchCurrentTeam = (name: string, update = false) => { if (currentTeam?.name !== name || update) { setIsLoading(true); getTeamByName(name, ['users', 'owns', 'defaultRoles', 'owner']) .then((res: AxiosResponse) => { - setCurrentTeam(res.data); - if (teams.length <= 0) { - fetchTeams(); + if (res.data) { + setCurrentTeam(res.data); + if (teams.length <= 0) { + fetchTeams(); + } + } else { + throw jsonData['api-error-messages']['unexpected-server-response']; } }) .catch((err: AxiosError) => { - if (err?.response?.data.code) { - setError(ERROR404); - } + const errMsg = getErrorText( + err, + jsonData['api-error-messages']['fetch-teams-error'] + ); + + setError(errMsg); + + handleShowErrorToast(errMsg); }) .finally(() => { setIsLoading(false); @@ -171,6 +203,12 @@ const TeamsPage = () => { } }; + /** + * Handle new data change + * @param data - team data + * @param forceSet - boolean value + * @returns - errorData + */ const onNewDataChange = (data: Team, forceSet = false) => { if (errorData || forceSet) { const errData: { [key: string]: string } = {}; @@ -200,6 +238,10 @@ const TeamsPage = () => { return {}; }; + /** + * Take Team data as input and create the team + * @param data - Team Data + */ const createNewTeam = (data: Team) => { const errData = onNewDataChange(data, true); if (!Object.values(errData).length) { @@ -212,13 +254,17 @@ const TeamsPage = () => { .then((res: AxiosResponse) => { if (res.data) { fetchTeams(); + } else { + throw jsonData['api-error-messages']['unexpected-server-response']; } }) .catch((error: AxiosError) => { - showToast({ - variant: 'error', - body: error.message ?? 'Something went wrong!', - }); + const errMsg = getErrorText( + error, + jsonData['api-error-messages']['create-team-error'] + ); + + handleShowErrorToast(errMsg); }) .finally(() => { setIsAddingTeam(false); @@ -226,20 +272,41 @@ const TeamsPage = () => { } }; - const createUsers = (data: Array) => { + /** + * Take users data as input and add users to team + * @param data + */ + const addUsersToTeam = (data: Array) => { const updatedTeam = { ...currentTeam, users: [...(currentTeam?.users as Array), ...data], }; const jsonPatch = compare(currentTeam as Team, updatedTeam); - patchTeamDetail(currentTeam?.id, jsonPatch).then((res: AxiosResponse) => { - if (res.data) { - fetchCurrentTeam(res.data.name, true); - } - }); - setIsAddingUsers(false); + patchTeamDetail(currentTeam?.id, jsonPatch) + .then((res: AxiosResponse) => { + if (res.data) { + fetchCurrentTeam(res.data.name, true); + } else { + throw jsonData['api-error-messages']['unexpected-server-response']; + } + }) + .catch((error: AxiosError) => { + const errMsg = getErrorText( + error, + jsonData['api-error-messages']['update-team-error'] + ); + + handleShowErrorToast(errMsg); + }) + .finally(() => { + setIsAddingUsers(false); + }); }; + /** + * Take user id as input to find out the user data and set it for delete + * @param id - user id + */ const deleteUserHandler = (id: string) => { const user = [...(currentTeam?.users as Array)].find( (u) => u.id === id @@ -247,7 +314,11 @@ const TeamsPage = () => { setDeletingUser({ user, state: true }); }; - const deleteUser = (id: string) => { + /** + * Take user id and remove that user from the team + * @param id - user id + */ + const removeUserFromTeam = (id: string) => { const users = [...(currentTeam?.users as Array)]; const newUsers = users.filter((user) => { return user.id !== id; @@ -261,54 +332,75 @@ const TeamsPage = () => { .then((res: AxiosResponse) => { if (res.data) { fetchCurrentTeam(res.data.name, true); + } else { + throw jsonData['api-error-messages']['unexpected-server-response']; } }) - .catch((err: AxiosError) => { - showToast({ - variant: 'error', - body: err.response?.data?.message ?? 'Error while removing user', - }); + .catch((error: AxiosError) => { + const errMsg = getErrorText( + error, + jsonData['api-error-messages']['update-team-error'] + ); + + handleShowErrorToast(errMsg); }) .finally(() => { setDeletingUser({ user: undefined, state: false }); }); }; + /** + * It will set current team for delete + */ const deleteTeamHandler = () => { const team = currentTeam; setDeletingTeam({ team: team, state: true }); }; + /** + * Take team id and delete the team + * @param id - Team id + */ const deleteTeamById = (id: string) => { deleteTeam(id) .then((res: AxiosResponse) => { if (res.data) { goToTeams(); } else { - handleShowErrorToast( - jsonData['api-error-messages']['delete-team-error'] - ); + throw jsonData['api-error-messages']['unexpected-server-response']; } }) .catch((err: AxiosError) => { - handleShowErrorToast( - err.response?.data?.message || - jsonData['api-error-messages']['delete-team-error'] + const errMsg = getErrorText( + err, + jsonData['api-error-messages']['delete-team-error'] ); + + handleShowErrorToast(errMsg); }) .finally(() => { setDeletingTeam({ team: undefined, state: false }); }); }; + /** + * Take tab value and return active class if tab value if equal to currentTab + * @param tab - tab value + * @returns - class for active tab + */ const getActiveTabClass = (tab: number) => { return tab === currentTab ? 'active' : ''; }; + + /** + * Handle current team route + * @param name - team name + */ const changeCurrentTeam = (name: string) => { history.push(getTeamDetailsPath(name)); }; - const getTabs = () => { + const Tabs = () => { return (
- {getTabs()} +
{currentTab === 1 && getUserCards()} @@ -740,7 +887,7 @@ const TeamsPage = () => { currentUser={currentTeam?.owner?.id} hasEditAccess={isOwner()} isJoinable={currentTeam?.isJoinable} - onSave={handleUpdateOwner} + onSave={handleUpdateTeam} /> )}
@@ -753,7 +900,7 @@ const TeamsPage = () => { }`} list={getUniqueUserList()} onCancel={() => setIsAddingUsers(false)} - onSave={(data) => createUsers(data)} + onSave={(data) => addUsersToTeam(data)} /> )}
@@ -807,7 +954,7 @@ const TeamsPage = () => { setDeletingUser({ user: undefined, state: false }) } onConfirm={() => { - deleteUser(deletingUser.user?.id as string); + removeUserFromTeam(deletingUser.user?.id as string); }} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/test/unit/mocks/fontawesome.mock.js b/openmetadata-ui/src/main/resources/ui/src/test/unit/mocks/fontawesome.mock.js new file mode 100644 index 00000000000..e28cb0da3f4 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/test/unit/mocks/fontawesome.mock.js @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Collate + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable */ +module.exports = { + FontAwesomeIcon: jest.fn().mockReturnValue('icon'), +};