diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/DataConsumerRole.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/DataConsumerRole.spec.js index 83a1db71a1b..9907b71e437 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/DataConsumerRole.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Features/DataConsumerRole.spec.js @@ -165,14 +165,13 @@ describe('DataConsumer Edit policy should work properly', () => { .should('be.visible') .click({ force: true }); verifyResponseStatusCode('@getUserPage', 200); - cy.get('[data-testid="left-panel"]').should( - 'contain', - `${CREDENTIALS.firstName}${CREDENTIALS.lastName}` - ); + cy.get( + '[data-testid="user-profile"] [data-testid="user-profile-details"]' + ).should('contain', `${CREDENTIALS.firstName}${CREDENTIALS.lastName}`); - cy.get('[data-testid="left-panel"]') - .should('be.visible') - .should('contain', policy); + cy.get( + '[data-testid="user-profile"] [data-testid="user-profile-inherited-roles"]' + ).should('contain', policy); }); it('Check if the new user has only edit access on description and tags', () => { diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddRoleAndAssignToUser.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddRoleAndAssignToUser.spec.js index 6f48f022705..49de7e8ede8 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddRoleAndAssignToUser.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddRoleAndAssignToUser.spec.js @@ -139,8 +139,8 @@ describe('Test Add role and assign it to the user', () => { cy.get(`[data-testid="${userName}"]`).should('be.visible').click(); verifyResponseStatusCode('@userDetailsPage', 200); - cy.get('[data-testid="left-panel"]') - .should('be.visible') - .should('contain', roleName); + cy.get( + '[data-testid="user-profile"] [data-testid="user-profile-roles"]' + ).should('contain', roleName); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Login.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Login.spec.js index 29eccfc1eae..2c0096aacda 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Login.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Login.spec.js @@ -82,10 +82,9 @@ describe('Login flow should work properly', () => { .should('be.visible') .click({ force: true }); verifyResponseStatusCode('@getUser', 200); - cy.get('[data-testid="left-panel"]').should( - 'contain', - `${CREDENTIALS.firstName}${CREDENTIALS.lastName}` - ); + cy.get( + '[data-testid="user-profile"] [data-testid="user-profile-details"]' + ).should('contain', `${CREDENTIALS.firstName}${CREDENTIALS.lastName}`); }); it('Signin using invalid credentials', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/Task-ic.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/Task-ic.svg deleted file mode 100644 index 7c7e8e15fc5..00000000000 --- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/Task-ic.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/all-activity-v2.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/all-activity-v2.svg new file mode 100644 index 00000000000..f20ce8c5437 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/all-activity-v2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/task-ic.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/task-ic.svg new file mode 100644 index 00000000000..5dbe5fea422 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/task-ic.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/teams-grey.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/teams-grey.svg index fee559c2cbc..28de11e97e4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/teams-grey.svg +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/teams-grey.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/user.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/user.svg index 1bfbda1eab4..0135eb1ba4e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/user.svg +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/user.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx index 1bf2dc1b588..02d9ffa5387 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Menu, Typography } from 'antd'; +import { Menu, Space, Typography } from 'antd'; import classNames from 'classnames'; import Loader from 'components/Loader/Loader'; import { TaskTab } from 'components/Task/TaskTab/TaskTab.component'; @@ -53,6 +53,11 @@ import { import { ReactComponent as CheckIcon } from '/assets/svg/ic-check.svg'; import { ReactComponent as TaskIcon } from '/assets/svg/ic-task.svg'; +import { ICON_DIMENSION } from 'constants/constants'; +import { ReactComponent as AllActivityIcon } from '/assets/svg/all-activity-v2.svg'; +import { ReactComponent as MentionIcon } from '/assets/svg/ic-mentions.svg'; +import { ReactComponent as TaskListIcon } from '/assets/svg/task-ic.svg'; + export const ActivityFeedTab = ({ fqn, owner, @@ -280,7 +285,11 @@ export const ActivityFeedTab = ({ { label: (
- {t('label.all')} + + + {t('label.all')} + + {getCountBadge( allCount, @@ -294,16 +303,20 @@ export const ActivityFeedTab = ({ }, { label: ( -
+ + {t('label.mention-plural')} -
+ ), key: 'mentions', }, { label: (
- {t('label.task-plural')} + + + {t('label.task-plural')} + {getCountBadge( tasksCount, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/activity-feed-tab.less b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/activity-feed-tab.less index 00cfe6c462b..54378086621 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/activity-feed-tab.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/activity-feed-tab.less @@ -19,6 +19,12 @@ .activity-feed-tab { display: flex; + .custom-menu.ant-menu-root.ant-menu-inline { + .ant-menu-item { + height: 40px; + } + } + .center-container { flex: 0 0 calc(50% - @left-side-panel-width / 2); height: @entity-details-tab-height; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/explore-search-card.less b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/explore-search-card.less index 3f83bc18f4d..b4a415c0995 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/explore-search-card.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/explore-search-card.less @@ -22,12 +22,4 @@ .entity-summary-details { font-size: 12px; } - - .max-two-lines { - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; - } } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TeamsSelectable/TeamsSelectable.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/TeamsSelectable/TeamsSelectable.interface.ts index 64bbc2411a2..7585db4f949 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TeamsSelectable/TeamsSelectable.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/TeamsSelectable/TeamsSelectable.interface.ts @@ -16,4 +16,5 @@ export interface TeamsSelectableProps { filterJoinable?: boolean; placeholder?: string; selectedTeams?: string[]; + maxValueCount?: number; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TeamsSelectable/TeamsSelectable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TeamsSelectable/TeamsSelectable.tsx index f87985c1dd5..516ec4c6d25 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TeamsSelectable/TeamsSelectable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TeamsSelectable/TeamsSelectable.tsx @@ -30,6 +30,7 @@ const TeamsSelectable = ({ type: t('label.team-plural-lowercase'), }), selectedTeams, + maxValueCount, }: TeamsSelectableProps) => { const [value, setValue] = useState>(); const [noTeam, setNoTeam] = useState(false); @@ -93,6 +94,7 @@ const TeamsSelectable = ({ treeDefaultExpandAll data-testid="team-select" dropdownStyle={{ maxHeight: 300, overflow: 'auto' }} + maxTagCount={maxValueCount} placeholder={placeholder} showCheckedStrategy={TreeSelect.SHOW_CHILD} style={{ width: '100%' }} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/ChangePasswordForm.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/ChangePasswordForm.tsx index f19bce9eb2c..cda5141861a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Users/ChangePasswordForm.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/ChangePasswordForm.tsx @@ -22,7 +22,7 @@ type ChangePasswordForm = { visible: boolean; onCancel: () => void; onSave: (data: ChangePasswordRequest) => void; - isLoggedinUser: boolean; + isLoggedInUser: boolean; isLoading: boolean; }; @@ -30,7 +30,7 @@ const ChangePasswordForm: React.FC = ({ visible, onCancel, onSave, - isLoggedinUser, + isLoggedInUser, isLoading, }) => { const { t } = useTranslation(); @@ -65,7 +65,7 @@ const ChangePasswordForm: React.FC = ({ name="change-password-form" validateMessages={VALIDATION_MESSAGES} onFinish={onSave}> - {isLoggedinUser && ( + {isLoggedInUser && ( { return jest.fn().mockReturnValue(

ProfilePicture

); }); +jest.mock( + './UsersProfile/UserProfileDetails/UserProfileDetails.component', + () => { + return jest.fn().mockReturnValue(
UserProfileDetails
); + } +); + +jest.mock('./UsersProfile/UserProfileImage/UserProfileImage.component', () => { + return jest.fn().mockReturnValue(
UserProfileImage
); +}); + +jest.mock( + './UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component', + () => { + return jest.fn().mockReturnValue(
UserProfileInheritedRoles
); + } +); + +jest.mock('./UsersProfile/UserProfileRoles/UserProfileRoles.component', () => { + return jest.fn().mockReturnValue(
UserProfileRoles
); +}); + +jest.mock('./UsersProfile/UserProfileTeams/UserProfileTeams.component', () => { + return jest.fn().mockReturnValue(
UserProfileTeams
); +}); + jest.mock('components/searched-data/SearchedData', () => { return jest.fn().mockReturnValue(

SearchedData

); }); @@ -132,7 +157,7 @@ const mockProp = { paging: mockPaging, postFeedHandler: postFeed, isAdminUser: false, - isLoggedinUser: false, + isLoggedInUser: false, isAuthDisabled: true, isUserEntitiesLoading: false, updateUserDetails, @@ -166,28 +191,24 @@ describe('Test User Component', () => { } ); - const leftPanel = await findByTestId(container, 'left-panel'); - - expect(leftPanel).toBeInTheDocument(); - }); - - it('Only admin can able to see tab for bot page', async () => { - const { container } = render( - , - { - wrapper: MemoryRouter, - } + const UserProfileDetails = await findByText( + container, + 'UserProfileDetails' ); + const UserProfileImage = await findByText(container, 'UserProfileImage'); + const UserProfileInheritedRoles = await findByText( + container, + 'UserProfileInheritedRoles' + ); + const UserProfileRoles = await findByText(container, 'UserProfileRoles'); - const tabs = await findByTestId(container, 'tabs'); - const leftPanel = await findByTestId(container, 'left-panel'); + const UserProfileTeams = await findByText(container, 'UserProfileTeams'); - expect(tabs).toBeInTheDocument(); - expect(leftPanel).toBeInTheDocument(); + expect(UserProfileDetails).toBeInTheDocument(); + expect(UserProfileImage).toBeInTheDocument(); + expect(UserProfileRoles).toBeInTheDocument(); + expect(UserProfileTeams).toBeInTheDocument(); + expect(UserProfileInheritedRoles).toBeInTheDocument(); }); it('Tab should not visible to normal user', async () => { @@ -199,36 +220,8 @@ describe('Test User Component', () => { ); const tabs = queryByTestId(container, 'tab'); - const leftPanel = await findByTestId(container, 'left-panel'); expect(tabs).not.toBeInTheDocument(); - expect(leftPanel).toBeInTheDocument(); - }); - - it('Should render non deleted teams', async () => { - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - - const teamFinance = await findByTestId(container, 'Finance'); - const teamDataPlatform = await findByTestId(container, 'Data_Platform'); - - expect(teamFinance).toBeInTheDocument(); - expect(teamDataPlatform).toBeInTheDocument(); - }); - - it('Should not render deleted teams', async () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); - }); - const deletedTeam = screen.queryByTestId('Customer_Support'); - - expect(deletedTeam).not.toBeInTheDocument(); }); it('Should check if cards are rendered', async () => { @@ -245,19 +238,6 @@ describe('Test User Component', () => { expect(datasetContainer).toBeInTheDocument(); }); - it('Should render inherited roles', async () => { - mockParams.tab = UserPageTabs.FOLLOWING; - const { container } = render( - , - { - wrapper: MemoryRouter, - } - ); - const inheritedRoles = await findByTestId(container, 'inherited-roles'); - - expect(inheritedRoles).toBeInTheDocument(); - }); - it('MyData tab should show loader if the data is loading', async () => { mockParams.tab = UserPageTabs.MY_DATA; const { container } = render( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx index 96b21c3a216..4f56f11ac9f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx @@ -11,74 +11,32 @@ * limitations under the License. */ -import { - Card, - Col, - Image, - Input, - Row, - Select, - Space, - Tabs, - Typography, -} from 'antd'; -import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg'; -import { ReactComponent as IconTeamsGrey } from 'assets/svg/teams-grey.svg'; -import { AxiosError } from 'axios'; +import { Col, Row, Tabs, Typography } from 'antd'; import ActivityFeedProvider from 'components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { ActivityFeedTab } from 'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder'; import EntitySummaryPanel from 'components/Explore/EntitySummaryPanel/EntitySummaryPanel.component'; -import InlineEdit from 'components/InlineEdit/InlineEdit.component'; import SearchedData from 'components/searched-data/SearchedData'; import { SearchedDataProps } from 'components/searched-data/SearchedData.interface'; import TabsLabel from 'components/TabsLabel/TabsLabel.component'; -import TeamsSelectable from 'components/TeamsSelectable/TeamsSelectable'; import { EntityType } from 'enums/entity.enum'; -import { isEmpty, noop, toLower } from 'lodash'; +import { isEmpty, noop } from 'lodash'; import { observer } from 'mobx-react'; -import React, { - Fragment, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useLocation, useParams } from 'react-router-dom'; -import { changePassword } from 'rest/auth-API'; -import { getRoles } from 'rest/rolesAPIV1'; -import { getEntityName } from 'utils/EntityUtils'; -import { - DE_ACTIVE_COLOR, - getUserPath, - PAGE_SIZE_LARGE, - TERM_ADMIN, -} from '../../constants/constants'; +import { getUserPath } from '../../constants/constants'; import { USER_PROFILE_TABS } from '../../constants/usersprofile.constants'; -import { AuthTypes } from '../../enums/signin.enum'; -import { - ChangePasswordRequest, - RequestType, -} from '../../generated/auth/changePasswordRequest'; -import { Role } from '../../generated/entity/teams/role'; -import { EntityReference } from '../../generated/entity/teams/user'; -import { getNonDeletedTeams } from '../../utils/CommonUtils'; -import { - getImageWithResolutionAndFallback, - ImageQuality, -} from '../../utils/ProfilerUtils'; -import SVGIcons, { Icons } from '../../utils/SvgUtils'; -import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils'; -import { useAuthContext } from '../authentication/auth-provider/AuthProvider'; -import Description from '../common/description/Description'; -import ProfilePicture from '../common/ProfilePicture/ProfilePicture'; import PageLayoutV1 from '../containers/PageLayoutV1'; import Loader from '../Loader/Loader'; -import ChangePasswordForm from './ChangePasswordForm'; import { Props, UserPageTabs } from './Users.interface'; import './Users.style.less'; import { userPageFilterList } from './Users.util'; +import UserProfileDetails from './UsersProfile/UserProfileDetails/UserProfileDetails.component'; +import UserProfileImage from './UsersProfile/UserProfileImage/UserProfileImage.component'; +import UserProfileInheritedRoles from './UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component'; +import UserProfileRoles from './UsersProfile/UserProfileRoles/UserProfileRoles.component'; +import UserProfileTeams from './UsersProfile/UserProfileTeams/UserProfileTeams.component'; const Users = ({ userData, @@ -86,43 +44,20 @@ const Users = ({ ownedEntities, isUserEntitiesLoading, updateUserDetails, - isAdminUser, - isLoggedinUser, - isAuthDisabled, username, handlePaginate, }: Props) => { const { tab = UserPageTabs.ACTIVITY } = useParams<{ tab: UserPageTabs }>(); - const [displayName, setDisplayName] = useState(userData.displayName); - const [isDisplayNameEdit, setIsDisplayNameEdit] = useState(false); - const [isDescriptionEdit, setIsDescriptionEdit] = useState(false); - const [isRolesEdit, setIsRolesEdit] = useState(false); - const [isTeamsEdit, setIsTeamsEdit] = useState(false); - const [selectedRoles, setSelectedRoles] = useState>([]); - const [selectedTeams, setSelectedTeams] = useState>([]); - const [roles, setRoles] = useState>([]); + const history = useHistory(); - const [isImgUrlValid, SetIsImgUrlValid] = useState(true); - const [isChangePassword, setIsChangePassword] = useState(false); const location = useLocation(); - const [isLoading, setIsLoading] = useState(false); - const [isRolesLoading, setIsRolesLoading] = useState(false); const [showSummaryPanel, setShowSummaryPanel] = useState(false); const [entityDetails, setEntityDetails] = useState(); - const { authConfig } = useAuthContext(); const { t } = useTranslation(); - const { isAuthProviderBasic } = useMemo(() => { - return { - isAuthProviderBasic: - authConfig?.provider === AuthTypes.BASIC || - authConfig?.provider === AuthTypes.LDAP, - }; - }, [authConfig]); - const tabs = useMemo(() => { return USER_PROFILE_TABS.map((data) => ({ label: , @@ -130,10 +65,6 @@ const Users = ({ })); }, []); - const onDisplayNameChange = (e: React.ChangeEvent) => { - setDisplayName(e.target.value); - }; - const activeTabHandler = (activeKey: string) => { // To reset search params appends from other page for proper navigation location.search = ''; @@ -145,508 +76,6 @@ const Users = ({ } }; - const handleDisplayNameChange = () => { - if (displayName !== userData.displayName) { - updateUserDetails({ displayName: displayName || '' }); - } - setIsDisplayNameEdit(false); - }; - - const handleDescriptionChange = async (description: string) => { - await updateUserDetails({ description }); - - setIsDescriptionEdit(false); - }; - - const handleRolesChange = () => { - // filter out the roles , and exclude the admin one - const updatedRoles = selectedRoles.filter( - (roleId) => roleId !== toLower(TERM_ADMIN) - ); - - // get the admin role and send it as boolean value `isAdmin=Boolean(isAdmin) - const isAdmin = selectedRoles.find( - (roleId) => roleId === toLower(TERM_ADMIN) - ); - updateUserDetails({ - roles: updatedRoles.map((roleId) => { - const role = roles.find((r) => r.id === roleId); - - return { id: roleId, type: 'role', name: role?.name || '' }; - }), - isAdmin: Boolean(isAdmin), - }); - - setIsRolesEdit(false); - }; - const handleTeamsChange = () => { - updateUserDetails({ - teams: selectedTeams.map((teamId) => { - return { id: teamId, type: 'team' }; - }), - }); - - setIsTeamsEdit(false); - }; - - const handleOnRolesChange = (value: string[]) => { - setSelectedRoles(value); - }; - - const handleOnTeamsChange = (value: string[]) => { - setSelectedTeams(value); - }; - - const handleChangePassword = async (data: ChangePasswordRequest) => { - try { - setIsLoading(true); - const sendData = { - ...data, - ...(isAdminUser && - !isLoggedinUser && { - username: userData.name, - requestType: RequestType.User, - }), - }; - await changePassword(sendData); - setIsChangePassword(false); - showSuccessToast( - t('server.update-entity-success', { entity: t('label.password') }) - ); - } catch (err) { - showErrorToast(err as AxiosError); - } finally { - setIsLoading(true); - } - }; - - const getDisplayNameComponent = () => { - if (isAdminUser || isLoggedinUser || isAuthDisabled) { - return ( -
- {isDisplayNameEdit ? ( - setIsDisplayNameEdit(false)} - onSave={handleDisplayNameChange}> - - - ) : ( - - - {userData.displayName || - t('label.add-entity', { entity: t('label.display-name') })} - - - - )} -
- ); - } else { - return ( -

- {getEntityName(userData as unknown as EntityReference)} -

- ); - } - }; - - const getDescriptionComponent = () => { - if (isAdminUser || isLoggedinUser || isAuthDisabled) { - return ( -
- setIsDescriptionEdit(false)} - onDescriptionEdit={() => setIsDescriptionEdit(true)} - onDescriptionUpdate={handleDescriptionChange} - /> -
- ); - } else { - return ( - - {userData.description || ( - - {t('label.no-entity', { - entity: t('label.description'), - })} - - )} - - ); - } - }; - - const getChangePasswordComponent = () => { - return ( -
- setIsChangePassword(true)}> - {t('label.change-entity', { entity: t('label.password-lowercase') })} - - - setIsChangePassword(false)} - onSave={(data) => handleChangePassword(data)} - /> -
- ); - }; - - const getTeamsComponent = () => { - const teamsElement = ( - - {getNonDeletedTeams(userData.teams ?? []).map((team, i) => ( -
- - - {getEntityName(team)} - -
- ))} - {isEmpty(userData.teams) && ( - {t('message.no-team-found')} - )} -
- ); - - if (!isAdminUser && !isAuthDisabled) { - return ( - -
- {t('label.team-plural')} -
-
- }> -
{teamsElement}
- - ); - } else { - return ( - -
- {t('label.team-plural')} -
- {!isTeamsEdit && ( - - )} -
- }> -
- {isTeamsEdit ? ( - setIsTeamsEdit(false)} - onSave={handleTeamsChange}> - - - ) : ( - teamsElement - )} -
- - ); - } - }; - - const getRolesComponent = () => { - const userRolesOption = roles?.map((role) => ({ - label: getEntityName(role as unknown as EntityReference), - value: role.id, - })); - if (!userData.isAdmin) { - userRolesOption.push({ - label: TERM_ADMIN, - value: toLower(TERM_ADMIN), - }); - } - - const rolesElement = ( - - {userData.isAdmin && ( -
- - {TERM_ADMIN} -
- )} - {userData.roles?.map((role, i) => ( -
- - - {getEntityName(role)} - -
- ))} - {!userData.isAdmin && isEmpty(userData.roles) && ( - - {t('message.no-roles-assigned')} - - )} -
- ); - - if (!isAdminUser && !isAuthDisabled) { - return ( - -
- {t('label.role-plural')} -
- - }> -
{rolesElement}
-
- ); - } else { - return ( - -
- {t('label.role-plural')} -
- {!isRolesEdit && ( - - )} - - }> -
- {isRolesEdit ? ( - setIsRolesEdit(false)} - onSave={handleRolesChange}> - + + ) : ( + + + + {hasEditPermission + ? userData.displayName ?? + t('label.add-entity', { entity: t('label.display-name') }) + : getEntityName(userData)} + + + + {hasEditPermission && ( + setIsDisplayNameEdit(true)} + /> + )} + + + ), + [ + userData, + isDisplayNameEdit, + hasEditPermission, + getEntityName, + onDisplayNameChange, + handleDisplayNameSave, + ] + ); + + const descriptionRenderComponent = useMemo( + () => + hasEditPermission ? ( + setIsDescriptionEdit(false)} + onDescriptionEdit={() => setIsDescriptionEdit(true)} + onDescriptionUpdate={handleDescriptionChange} + /> + ) : ( + + {userData.description ?? ( + + {t('label.no-entity', { + entity: t('label.description'), + })} + + )} + + ), + [ + userData, + isAdminUser, + isDescriptionEdit, + hasEditPermission, + getEntityName, + handleDescriptionChange, + ] + ); + + const changePasswordRenderComponent = useMemo( + () => + isAuthProviderBasic && + (isAdminUser || isLoggedInUser) && ( + + ), + [isAuthProviderBasic, isAdminUser, isLoggedInUser] + ); + + const handleChangePassword = async (data: ChangePasswordRequest) => { + try { + setIsLoading(true); + const sendData = { + ...data, + ...(isAdminUser && + !isLoggedInUser && { + username: userData.name, + requestType: RequestType.User, + }), + }; + await changePassword(sendData); + setIsChangePassword(false); + showSuccessToast( + t('server.update-entity-success', { entity: t('label.password') }) + ); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setIsLoading(true); + } + }; + + return ( + + + {displayNameRenderComponent} + + {userData.email} + + + + + {descriptionRenderComponent} + {changePasswordRenderComponent} + + + setIsChangePassword(false)} + onSave={(data) => handleChangePassword(data)} + /> + + ); +}; + +export default UserProfileDetails; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileDetails/UserProfileDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileDetails/UserProfileDetails.interface.ts new file mode 100644 index 00000000000..274a37036cf --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileDetails/UserProfileDetails.interface.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { User } from 'generated/entity/teams/user'; + +export interface UserProfileDetailsProps { + userData: { + email: string; + name: string; + displayName?: string; + description?: string; + }; + updateUserDetails: (data: Partial) => Promise; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx new file mode 100644 index 00000000000..56de618c5bd --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx @@ -0,0 +1,64 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Image } from 'antd'; +import ProfilePicture from 'components/common/ProfilePicture/ProfilePicture'; +import React, { useEffect, useMemo, useState } from 'react'; +import { + getImageWithResolutionAndFallback, + ImageQuality, +} from 'utils/ProfilerUtils'; +import { UserProfileImageProps } from './UserProfileImage.interface'; + +const UserProfileImage = ({ userData }: UserProfileImageProps) => { + const [isImgUrlValid, SetIsImgUrlValid] = useState(true); + + const image = useMemo( + () => + getImageWithResolutionAndFallback(ImageQuality['6x'], userData?.images), + [userData?.images] + ); + + useEffect(() => { + if (image) { + SetIsImgUrlValid(true); + } + }, [image]); + + return ( +
+ {isImgUrlValid ? ( + profile { + SetIsImgUrlValid(false); + }} + /> + ) : ( + + )} +
+ ); +}; + +export default UserProfileImage; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileImage/UserProfileImage.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileImage/UserProfileImage.interface.ts new file mode 100644 index 00000000000..65d4410baca --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileImage/UserProfileImage.interface.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ImageList } from 'generated/type/profile'; + +export interface UserProfileImageProps { + userData: { + id?: string; + name?: string; + displayName?: string; + images?: ImageList; + }; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component.tsx new file mode 100644 index 00000000000..9ebc52f6491 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Card, Typography } from 'antd'; +import Chip from 'components/common/Chip/Chip.component'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as UserIcons } from '../../../../assets/svg/user.svg'; +import { UserProfileInheritedRolesProps } from './UserProfileInheritedRoles.interface'; + +const UserProfileInheritedRoles = ({ + inheritedRoles, +}: UserProfileInheritedRolesProps) => { + const { t } = useTranslation(); + + return ( + + {t('label.inherited-role-plural')} + + }> + } + noDataPlaceholder={t('message.no-inherited-roles-found')} + /> + + ); +}; + +export default UserProfileInheritedRoles; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.interface.ts new file mode 100644 index 00000000000..e5c59d116ca --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.interface.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { User } from 'generated/entity/teams/user'; + +export interface UserProfileInheritedRolesProps { + inheritedRoles: User['inheritedRoles']; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx new file mode 100644 index 00000000000..f3f136e4a44 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx @@ -0,0 +1,193 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Card, Select, Space, Typography } from 'antd'; +import { AxiosError } from 'axios'; +import Chip from 'components/common/Chip/Chip.component'; +import InlineEdit from 'components/InlineEdit/InlineEdit.component'; +import { + DE_ACTIVE_COLOR, + ICON_DIMENSION, + PAGE_SIZE_LARGE, + TERM_ADMIN, +} from 'constants/constants'; +import { Role } from 'generated/entity/teams/role'; +import { useAuth } from 'hooks/authHooks'; +import { isEmpty, toLower } from 'lodash'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { getRoles } from 'rest/rolesAPIV1'; +import { getEntityName } from 'utils/EntityUtils'; +import { showErrorToast } from 'utils/ToastUtils'; +import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg'; +import { ReactComponent as UserIcons } from '../../../../assets/svg/user.svg'; +import { UserProfileRolesProps } from './UserProfileRoles.interface'; + +const UserProfileRoles = ({ + isUserAdmin, + userRoles, + updateUserDetails, +}: UserProfileRolesProps) => { + const { t } = useTranslation(); + + const { isAdminUser } = useAuth(); + + const [isRolesEdit, setIsRolesEdit] = useState(false); + const [isRolesLoading, setIsRolesLoading] = useState(false); + const [selectedRoles, setSelectedRoles] = useState([]); + const [roles, setRoles] = useState([]); + + const useRolesOption = useMemo(() => { + const options = roles?.map((role) => ({ + label: getEntityName(role), + value: role.id, + })); + + if (!isUserAdmin) { + options.push({ + label: TERM_ADMIN, + value: toLower(TERM_ADMIN), + }); + } + + return options; + }, [roles]); + + const fetchRoles = async () => { + setIsRolesLoading(true); + try { + const response = await getRoles( + '', + undefined, + undefined, + false, + PAGE_SIZE_LARGE + ); + setRoles(response.data); + } catch (err) { + showErrorToast( + err as AxiosError, + t('server.entity-fetch-error', { + entity: t('label.role-plural'), + }) + ); + } finally { + setIsRolesLoading(false); + } + }; + + const handleRolesSave = () => { + // filter out the roles , and exclude the admin one + const updatedRoles = selectedRoles.filter( + (roleId) => roleId !== toLower(TERM_ADMIN) + ); + + // get the admin role and send it as boolean value `isAdmin=Boolean(isAdmin) + const isAdmin = selectedRoles.find( + (roleId) => roleId === toLower(TERM_ADMIN) + ); + updateUserDetails({ + roles: updatedRoles.map((roleId) => { + const role = roles.find((r) => r.id === roleId); + + return { id: roleId, type: 'role', name: role?.name ?? '' }; + }), + isAdmin: Boolean(isAdmin), + }); + + setIsRolesEdit(false); + }; + + const rolesRenderElement = useMemo( + () => ( + } + noDataPlaceholder={t('message.no-roles-assigned')} + showNoDataPlaceholder={!isUserAdmin} + /> + ), + [userRoles, isUserAdmin] + ); + + useEffect(() => { + const defaultUserRoles = [ + ...(userRoles?.map((role) => role.id) ?? []), + ...(isUserAdmin ? [toLower(TERM_ADMIN)] : []), + ]; + + setSelectedRoles(defaultUserRoles); + }, [isUserAdmin, userRoles]); + + useEffect(() => { + if (isRolesEdit && isEmpty(roles)) { + fetchRoles(); + } + }, [isRolesEdit, roles]); + + return ( + + + {t('label.role-plural')} + + {!isRolesEdit && isAdminUser && ( + setIsRolesEdit(true)} + /> + )} + + }> +
+ {isRolesEdit && isAdminUser ? ( + setIsRolesEdit(false)} + onSave={handleRolesSave}> +