mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-27 09:58:14 +00:00
Feature/users and groups UI updated as per new design (#4134)
This commit is contained in:
parent
585aad1aac
commit
413990d3e8
22
datahub-web-react/src/app/entity/user/UserAssets.tsx
Normal file
22
datahub-web-react/src/app/entity/user/UserAssets.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EmbeddedListSearch } from '../shared/components/styled/search/EmbeddedListSearch';
|
||||
import { useEntityData } from '../shared/EntityContext';
|
||||
|
||||
const UserAssetsWrapper = styled.div`
|
||||
height: calc(100vh - 114px);
|
||||
overflow: auto;
|
||||
`;
|
||||
export const UserAssets = () => {
|
||||
const { urn } = useEntityData();
|
||||
|
||||
return (
|
||||
<UserAssetsWrapper>
|
||||
<EmbeddedListSearch
|
||||
fixedFilter={{ field: 'owners', value: urn }}
|
||||
emptySearchQuery="*"
|
||||
placeholderText="Filter domain entities..."
|
||||
/>
|
||||
</UserAssetsWrapper>
|
||||
);
|
||||
};
|
||||
224
datahub-web-react/src/app/entity/user/UserEditProfileModal.tsx
Normal file
224
datahub-web-react/src/app/entity/user/UserEditProfileModal.tsx
Normal file
@ -0,0 +1,224 @@
|
||||
import React, { useState } from 'react';
|
||||
import { message, Button, Input, Modal, Typography, Form } from 'antd';
|
||||
import { useUpdateCorpUserPropertiesMutation } from '../../../graphql/user.generated';
|
||||
|
||||
type PropsData = {
|
||||
name: string | undefined;
|
||||
title: string | undefined;
|
||||
image: string | undefined;
|
||||
team: string | undefined;
|
||||
email: string | undefined;
|
||||
slack: string | undefined;
|
||||
phone: string | undefined;
|
||||
urn: string | undefined;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
editModalData: PropsData;
|
||||
};
|
||||
/** Regex Validations */
|
||||
export const USER_NAME_REGEX = new RegExp('^[a-zA-Z ]*$');
|
||||
|
||||
export default function UserEditProfileModal({ visible, onClose, onSave, editModalData }: Props) {
|
||||
const [updateCorpUserPropertiesMutation] = useUpdateCorpUserPropertiesMutation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [saveButtonEnabled, setSaveButtonEnabled] = useState(true);
|
||||
const [data, setData] = useState<PropsData>({
|
||||
name: editModalData.name,
|
||||
title: editModalData.title,
|
||||
image: editModalData.image,
|
||||
team: editModalData.team,
|
||||
email: editModalData.email,
|
||||
slack: editModalData.slack,
|
||||
phone: editModalData.phone,
|
||||
urn: editModalData.urn,
|
||||
});
|
||||
|
||||
// save changes function
|
||||
const onSaveChanges = () => {
|
||||
updateCorpUserPropertiesMutation({
|
||||
variables: {
|
||||
urn: editModalData?.urn || '',
|
||||
input: {
|
||||
displayName: data.name,
|
||||
title: data.title,
|
||||
pictureLink: data.image,
|
||||
teams: data.team?.split(','),
|
||||
email: data.email,
|
||||
slack: data.slack,
|
||||
phone: data.phone,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to Save changes!: \n ${e.message || ''}`, duration: 3 });
|
||||
})
|
||||
.finally(() => {
|
||||
message.success({
|
||||
content: `Changes saved.`,
|
||||
duration: 3,
|
||||
});
|
||||
onSave(); // call the refetch function once save
|
||||
// clear the values from edit profile form
|
||||
setData({
|
||||
name: '',
|
||||
title: '',
|
||||
image: '',
|
||||
team: '',
|
||||
email: '',
|
||||
slack: '',
|
||||
phone: '',
|
||||
urn: '',
|
||||
});
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Edit Profile"
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} type="text">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSaveChanges} disabled={saveButtonEnabled}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={{ ...editModalData }}
|
||||
autoComplete="off"
|
||||
layout="vertical"
|
||||
onFieldsChange={() =>
|
||||
setSaveButtonEnabled(form.getFieldsError().some((field) => field.errors.length > 0))
|
||||
}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={<Typography.Text strong>Name</Typography.Text>}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Enter a display name.',
|
||||
},
|
||||
{ whitespace: true },
|
||||
{ min: 2, max: 50 },
|
||||
{
|
||||
pattern: USER_NAME_REGEX,
|
||||
message: '',
|
||||
},
|
||||
]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
placeholder="John Smith"
|
||||
value={data.name}
|
||||
onChange={(event) => setData({ ...data, name: event.target.value })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="title"
|
||||
label={<Typography.Text strong>Title/Role</Typography.Text>}
|
||||
rules={[{ whitespace: true }, { min: 2, max: 50 }]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
placeholder="Data Analyst"
|
||||
value={data.title}
|
||||
onChange={(event) => setData({ ...data, title: event.target.value })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="image"
|
||||
label={<Typography.Text strong>Image URL</Typography.Text>}
|
||||
rules={[{ whitespace: true }, { type: 'url', message: 'not valid url' }]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
placeholder="https://www.example.com/photo.png"
|
||||
value={data.image}
|
||||
onChange={(event) => setData({ ...data, image: event.target.value })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="team"
|
||||
label={<Typography.Text strong>Team</Typography.Text>}
|
||||
rules={[{ whitespace: true }, { min: 2, max: 50 }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="Product Engineering"
|
||||
value={data.team}
|
||||
onChange={(event) => setData({ ...data, team: event.target.value })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label={<Typography.Text strong>Email</Typography.Text>}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Enter your email',
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
message: 'Please enter valid email',
|
||||
},
|
||||
{ whitespace: true },
|
||||
{ min: 2, max: 50 },
|
||||
]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
placeholder="john.smith@example.com"
|
||||
value={data.email}
|
||||
onChange={(event) => setData({ ...data, email: event.target.value })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="slack"
|
||||
label={<Typography.Text strong>Slack</Typography.Text>}
|
||||
rules={[{ whitespace: true }, { min: 2, max: 50 }]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
placeholder="john_smith"
|
||||
value={data.slack}
|
||||
onChange={(event) => setData({ ...data, slack: event.target.value })}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="phone"
|
||||
label={<Typography.Text strong>Phone</Typography.Text>}
|
||||
rules={[
|
||||
{
|
||||
pattern: new RegExp('^(?=.*[0-9])[- +()0-9]+$'),
|
||||
message: 'not valid phone number',
|
||||
},
|
||||
{
|
||||
min: 5,
|
||||
max: 15,
|
||||
},
|
||||
]}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
placeholder="444-999-9999"
|
||||
value={data.phone}
|
||||
onChange={(event) => setData({ ...data, phone: event.target.value })}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import { List, Pagination, Row, Space, Typography } from 'antd';
|
||||
import { Col, Pagination, Row, Tooltip } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { useGetUserGroupsLazyQuery } from '../../../graphql/user.generated';
|
||||
import { CorpGroup, EntityRelationshipsResult, EntityType } from '../../../types.generated';
|
||||
import { useEntityRegistry } from '../../useEntityRegistry';
|
||||
import { PreviewType } from '../Entity';
|
||||
|
||||
type Props = {
|
||||
urn: string;
|
||||
@ -12,30 +12,67 @@ type Props = {
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
const GroupList = styled(List)`
|
||||
&&& {
|
||||
const GroupsViewWrapper = styled.div`
|
||||
height: calc(100vh - 173px);
|
||||
overflow-y: auto;
|
||||
|
||||
.user-group-pagination {
|
||||
justify-content: center;
|
||||
bottom: 24px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
border-color: ${(props) => props.theme.styles['border-color-base']};
|
||||
margin-top: 12px;
|
||||
margin-bottom: 28px;
|
||||
padding: 24px 32px;
|
||||
box-shadow: ${(props) => props.theme.styles['box-shadow']};
|
||||
}
|
||||
& li {
|
||||
padding-top: 28px;
|
||||
padding-bottom: 28px;
|
||||
}
|
||||
& li:not(:last-child) {
|
||||
border-bottom: 1.5px solid #ededed;
|
||||
left: 50%;
|
||||
-webkit-transform: translateX(-50%);
|
||||
-moz-transform: translateX(-50%);
|
||||
-webkit-transform: translateX(-50%);
|
||||
-ms-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
`;
|
||||
|
||||
const GroupsView = styled(Space)`
|
||||
width: 100%;
|
||||
margin-bottom: 32px;
|
||||
padding-top: 28px;
|
||||
const GroupItemColumn = styled(Col)`
|
||||
padding: 10px;
|
||||
`;
|
||||
|
||||
const GroupItem = styled.div`
|
||||
border: 1px solid #d9d9d9;
|
||||
padding: 10px;
|
||||
min-height: 107px;
|
||||
max-height: 107px;
|
||||
|
||||
.title-row {
|
||||
padding: 9px 11px 9px 11px;
|
||||
}
|
||||
.description-row {
|
||||
padding: 2px 13px;
|
||||
}
|
||||
`;
|
||||
|
||||
const GroupTitle = styled.span`
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: bold;
|
||||
color: #262626;
|
||||
`;
|
||||
|
||||
const GroupMember = styled.span`
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 23px;
|
||||
color: #8c8c8c;
|
||||
padding-left: 7px;
|
||||
`;
|
||||
|
||||
const GroupDescription = styled.span`
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #262626;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
height: 43px;
|
||||
`;
|
||||
export default function UserGroups({ urn, initialRelationships, pageSize }: Props) {
|
||||
const [page, setPage] = useState(1);
|
||||
const entityRegistry = useEntityRegistry();
|
||||
@ -53,19 +90,35 @@ export default function UserGroups({ urn, initialRelationships, pageSize }: Prop
|
||||
const userGroups = relationships?.relationships?.map((rel) => rel.entity as CorpGroup) || [];
|
||||
|
||||
return (
|
||||
<GroupsView direction="vertical" size="middle">
|
||||
<Typography.Title level={3}>Group Membership</Typography.Title>
|
||||
<Row justify="center">
|
||||
<GroupList
|
||||
dataSource={userGroups}
|
||||
split={false}
|
||||
renderItem={(item, _) => (
|
||||
<List.Item>
|
||||
{entityRegistry.renderPreview(EntityType.CorpGroup, PreviewType.PREVIEW, item)}
|
||||
</List.Item>
|
||||
)}
|
||||
bordered
|
||||
/>
|
||||
<GroupsViewWrapper>
|
||||
<Row justify="space-between">
|
||||
{userGroups &&
|
||||
userGroups.map((item) => {
|
||||
return (
|
||||
<GroupItemColumn xl={8} lg={8} md={12} sm={12} xs={24} key={item.urn}>
|
||||
<Link to={entityRegistry.getEntityUrl(EntityType.CorpGroup, item.urn)}>
|
||||
<GroupItem>
|
||||
<Row className="title-row">
|
||||
<GroupTitle>{item.info?.displayName || item.name}</GroupTitle>
|
||||
<GroupMember>
|
||||
{item.relationships?.total}
|
||||
{item.relationships?.total === 1 ? ' member' : ' members'}
|
||||
</GroupMember>
|
||||
</Row>
|
||||
<Row className="description-row">
|
||||
<GroupDescription>
|
||||
<Tooltip title={item.info?.description}>
|
||||
{item.info?.description}
|
||||
</Tooltip>
|
||||
</GroupDescription>
|
||||
</Row>
|
||||
</GroupItem>
|
||||
</Link>
|
||||
</GroupItemColumn>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
<Row className="user-group-pagination">
|
||||
<Pagination
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
@ -75,6 +128,6 @@ export default function UserGroups({ urn, initialRelationships, pageSize }: Prop
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
</Row>
|
||||
</GroupsView>
|
||||
</GroupsViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
337
datahub-web-react/src/app/entity/user/UserInfoSideBar.tsx
Normal file
337
datahub-web-react/src/app/entity/user/UserInfoSideBar.tsx
Normal file
@ -0,0 +1,337 @@
|
||||
import { Divider, message, Space, Button, Tag, Typography } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EditOutlined, MailOutlined, PhoneOutlined, SlackOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useUpdateCorpUserPropertiesMutation } from '../../../graphql/user.generated';
|
||||
import { EntityType } from '../../../types.generated';
|
||||
|
||||
import UserEditProfileModal from './UserEditProfileModal';
|
||||
import { ExtendedEntityRelationshipsResult } from './type';
|
||||
import CustomAvatar from '../../shared/avatar/CustomAvatar';
|
||||
import { useEntityRegistry } from '../../useEntityRegistry';
|
||||
import { useGetAuthenticatedUser } from '../../useGetAuthenticatedUser';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
type SideBarData = {
|
||||
photoUrl: string | undefined;
|
||||
avatarName: string | undefined;
|
||||
name: string | undefined;
|
||||
role: string | undefined;
|
||||
team: string | undefined;
|
||||
email: string | undefined;
|
||||
slack: string | undefined;
|
||||
phone: string | undefined;
|
||||
aboutText: string | undefined;
|
||||
groupsDetails: ExtendedEntityRelationshipsResult;
|
||||
urn: string | undefined;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
sideBarData: SideBarData;
|
||||
refetch: () => void;
|
||||
};
|
||||
|
||||
const AVATAR_STYLE = { marginTop: '14px' };
|
||||
|
||||
/**
|
||||
* Styled Components
|
||||
*/
|
||||
export const SideBar = styled.div`
|
||||
padding: 0 0 0 17px;
|
||||
text-align: center;
|
||||
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
height: calc(100vh - 60px);
|
||||
position: relative;
|
||||
|
||||
&&& .ant-avatar.ant-avatar-icon {
|
||||
font-size: 46px !important;
|
||||
}
|
||||
|
||||
.divider-infoSection {
|
||||
margin: 18px 0px 18px 0;
|
||||
}
|
||||
.divider-aboutSection {
|
||||
margin: 23px 0px 11px 0;
|
||||
}
|
||||
.divider-groupsSection {
|
||||
margin: 23px 0px 11px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SideBarSubSection = styled.div`
|
||||
height: calc(100vh - 135px);
|
||||
overflow: auto;
|
||||
padding-right: 18px;
|
||||
&.fullView {
|
||||
height: calc(100vh - 70px);
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 1px;
|
||||
background: #f1f1f1;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #c3c3c3;
|
||||
-webkit-border-radius: 1ex;
|
||||
-webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
`;
|
||||
|
||||
export const EmptyValue = styled.div`
|
||||
&:after {
|
||||
content: 'None';
|
||||
color: #b7b7b7;
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Name = styled.div`
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
color: #262626;
|
||||
margin: 13px 0 7px 0;
|
||||
`;
|
||||
|
||||
export const Role = styled.div`
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #595959;
|
||||
margin-bottom: 7px;
|
||||
`;
|
||||
|
||||
export const Team = styled.div`
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #8c8c8c;
|
||||
`;
|
||||
|
||||
export const SocialDetails = styled.div`
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #262626;
|
||||
text-align: left;
|
||||
margin: 6px 0;
|
||||
`;
|
||||
|
||||
export const EditProfileButton = styled.div`
|
||||
bottom: 24px;
|
||||
position: absolute;
|
||||
right: 27px;
|
||||
width: 80%;
|
||||
left: 50%;
|
||||
-webkit-transform: translateX(-50%);
|
||||
-moz-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #262626;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AboutSection = styled.div`
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #262626;
|
||||
`;
|
||||
|
||||
export const AboutSectionText = styled.div`
|
||||
font-size: 12px;
|
||||
font-weight: 100;
|
||||
line-height: 15px;
|
||||
padding: 5px 0;
|
||||
|
||||
&&& .ant-typography {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&&& .ant-typography-edit-content {
|
||||
padding-left: 15px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const GroupsSection = styled.div`
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #262626;
|
||||
`;
|
||||
|
||||
export const TagsSection = styled.div`
|
||||
height: calc(75vh - 460px);
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
export const NoDataFound = styled.span`
|
||||
font-size: 12px;
|
||||
color: #262626;
|
||||
font-weight: 100;
|
||||
`;
|
||||
|
||||
export const Tags = styled.div`
|
||||
margin-top: 5px;
|
||||
`;
|
||||
|
||||
export const GroupsSeeMoreText = styled.span`
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Responsible for reading & writing users.
|
||||
*/
|
||||
export default function UserInfoSideBar({ sideBarData, refetch }: Props) {
|
||||
const { name, aboutText, avatarName, email, groupsDetails, phone, photoUrl, role, slack, team, urn } = sideBarData;
|
||||
|
||||
const [updateCorpUserPropertiesMutation] = useUpdateCorpUserPropertiesMutation();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
const [groupSectionExpanded, setGroupSectionExpanded] = useState(false);
|
||||
const [editProfileModal, showEditProfileModal] = useState(false);
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const [editableAboutMeText, setEditableAboutMeText] = useState<string | undefined>('');
|
||||
const me = useGetAuthenticatedUser();
|
||||
const showEditProfileButton = me?.corpUser?.urn === urn;
|
||||
|
||||
const getEditModalData = {
|
||||
urn,
|
||||
name,
|
||||
title: role,
|
||||
team,
|
||||
email,
|
||||
image: photoUrl,
|
||||
slack,
|
||||
phone,
|
||||
};
|
||||
|
||||
// About Text save
|
||||
const onSaveAboutMe = (inputString) => {
|
||||
setEditableAboutMeText(inputString);
|
||||
updateCorpUserPropertiesMutation({
|
||||
variables: {
|
||||
urn: urn || '',
|
||||
input: {
|
||||
aboutMe: inputString,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to Save changes!: \n ${e.message || ''}`, duration: 3 });
|
||||
})
|
||||
.finally(() => {
|
||||
message.success({
|
||||
content: `Changes saved.`,
|
||||
duration: 3,
|
||||
});
|
||||
refetch();
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<SideBar>
|
||||
<SideBarSubSection className={showEditProfileButton ? '' : 'fullView'}>
|
||||
<CustomAvatar size={160} photoUrl={photoUrl} name={avatarName} style={AVATAR_STYLE} />
|
||||
<Name>{name || <EmptyValue />}</Name>
|
||||
<Role>{role || <EmptyValue />}</Role>
|
||||
<Team>{team || <EmptyValue />}</Team>
|
||||
<Divider className="divider-infoSection" />
|
||||
<SocialDetails>
|
||||
<Space>
|
||||
<MailOutlined />
|
||||
{email || <EmptyValue />}
|
||||
</Space>
|
||||
</SocialDetails>
|
||||
<SocialDetails>
|
||||
<Space>
|
||||
<SlackOutlined />
|
||||
{slack || <EmptyValue />}
|
||||
</Space>
|
||||
</SocialDetails>
|
||||
<SocialDetails>
|
||||
<Space>
|
||||
<PhoneOutlined />
|
||||
{phone || <EmptyValue />}
|
||||
</Space>
|
||||
</SocialDetails>
|
||||
<Divider className="divider-aboutSection" />
|
||||
<AboutSection>
|
||||
About
|
||||
<AboutSectionText>
|
||||
<Paragraph
|
||||
editable={{ onChange: onSaveAboutMe }}
|
||||
ellipsis={{ rows: 2, expandable: true, symbol: 'Read more' }}
|
||||
>
|
||||
{aboutText || <EmptyValue />}
|
||||
</Paragraph>
|
||||
</AboutSectionText>
|
||||
</AboutSection>
|
||||
<Divider className="divider-groupsSection" />
|
||||
<GroupsSection>
|
||||
Groups
|
||||
<TagsSection>
|
||||
{groupsDetails?.relationships.length === 0 && <NoDataFound>No Groups</NoDataFound>}
|
||||
{!groupSectionExpanded &&
|
||||
groupsDetails?.relationships.slice(0, 2).map((item) => {
|
||||
return (
|
||||
<Link to={entityRegistry.getEntityUrl(EntityType.CorpGroup, item.entity.urn)}>
|
||||
<Tags>
|
||||
<Tag>
|
||||
{item.entity.info.displayName || item.entity.name || <EmptyValue />}
|
||||
</Tag>
|
||||
</Tags>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{groupSectionExpanded &&
|
||||
groupsDetails?.relationships.length > 2 &&
|
||||
groupsDetails?.relationships.map((item) => {
|
||||
return (
|
||||
<Tags>
|
||||
<Tag>
|
||||
{item.entity.info.displayName || item.entity.name || <EmptyValue />}
|
||||
</Tag>
|
||||
</Tags>
|
||||
);
|
||||
})}
|
||||
{!groupSectionExpanded && groupsDetails?.relationships.length > 2 && (
|
||||
<GroupsSeeMoreText onClick={() => setGroupSectionExpanded(!groupSectionExpanded)}>
|
||||
{`+${groupsDetails?.relationships.length - 2} more`}
|
||||
</GroupsSeeMoreText>
|
||||
)}
|
||||
</TagsSection>
|
||||
</GroupsSection>
|
||||
</SideBarSubSection>
|
||||
{showEditProfileButton && (
|
||||
<EditProfileButton>
|
||||
<Button icon={<EditOutlined />} onClick={() => showEditProfileModal(true)}>
|
||||
Edit Profile
|
||||
</Button>
|
||||
</EditProfileButton>
|
||||
)}
|
||||
</SideBar>
|
||||
{/* Modal */}
|
||||
<UserEditProfileModal
|
||||
visible={editProfileModal}
|
||||
onClose={() => showEditProfileModal(false)}
|
||||
onSave={() => {
|
||||
refetch();
|
||||
}}
|
||||
editModalData={getEditModalData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,26 +1,57 @@
|
||||
import { Alert } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import UserHeader from './UserHeader';
|
||||
import { Alert, Col, Row } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import useUserParams from '../../shared/entitySearch/routingUtils/useUserParams';
|
||||
import { useGetUserQuery } from '../../../graphql/user.generated';
|
||||
import { useGetAllEntitySearchResults } from '../../../utils/customGraphQL/useGetAllEntitySearchResults';
|
||||
import { Message } from '../../shared/Message';
|
||||
import RelatedEntityResults from '../../shared/entitySearch/RelatedEntityResults';
|
||||
import { LegacyEntityProfile } from '../../shared/LegacyEntityProfile';
|
||||
import { CorpUser, EntityType, SearchResult, EntityRelationshipsResult } from '../../../types.generated';
|
||||
import { EntityRelationshipsResult } from '../../../types.generated';
|
||||
import UserGroups from './UserGroups';
|
||||
import { useEntityRegistry } from '../../useEntityRegistry';
|
||||
import { RoutedTabs } from '../../shared/RoutedTabs';
|
||||
import { UserAssets } from './UserAssets';
|
||||
import { ExtendedEntityRelationshipsResult } from './type';
|
||||
import { decodeUrn } from '../shared/utils';
|
||||
import UserInfoSideBar from './UserInfoSideBar';
|
||||
|
||||
const messageStyle = { marginTop: '10%' };
|
||||
export interface Props {
|
||||
onTabChange: (selectedTab: string) => void;
|
||||
}
|
||||
|
||||
export enum TabType {
|
||||
Ownership = 'Ownership',
|
||||
Assets = 'Assets',
|
||||
Groups = 'Groups',
|
||||
}
|
||||
const ENABLED_TAB_TYPES = [TabType.Ownership, TabType.Groups];
|
||||
const ENABLED_TAB_TYPES = [TabType.Assets, TabType.Groups];
|
||||
|
||||
const GROUP_PAGE_SIZE = 20;
|
||||
const MESSAGE_STYLE = { marginTop: '10%' };
|
||||
|
||||
/**
|
||||
* Styled Components
|
||||
*/
|
||||
const UserProfileWrapper = styled.div`
|
||||
&&& .ant-tabs-nav {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
color: #262626;
|
||||
height: calc(100vh - 60px);
|
||||
|
||||
&&& .ant-tabs > .ant-tabs-nav .ant-tabs-nav-wrap {
|
||||
padding-left: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const EmptyValue = styled.div`
|
||||
&:after {
|
||||
content: 'None';
|
||||
color: #b7b7b7;
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Responsible for reading & writing users.
|
||||
@ -28,46 +59,36 @@ const GROUP_PAGE_SIZE = 20;
|
||||
export default function UserProfile() {
|
||||
const { urn: encodedUrn } = useUserParams();
|
||||
const urn = decodeUrn(encodedUrn);
|
||||
const { loading, error, data } = useGetUserQuery({ variables: { urn, groupsCount: GROUP_PAGE_SIZE } });
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const username = data?.corpUser?.username;
|
||||
|
||||
const { loading, error, data, refetch } = useGetUserQuery({ variables: { urn, groupsCount: GROUP_PAGE_SIZE } });
|
||||
|
||||
const username = data?.corpUser?.username;
|
||||
const ownershipResult = useGetAllEntitySearchResults({
|
||||
query: `owners:${username}`,
|
||||
});
|
||||
|
||||
const groupMemberRelationships = data?.corpUser?.relationships as EntityRelationshipsResult;
|
||||
const groupsDetails = data?.corpUser?.relationships as ExtendedEntityRelationshipsResult;
|
||||
|
||||
const contentLoading =
|
||||
Object.keys(ownershipResult).some((type) => {
|
||||
return ownershipResult[type].loading;
|
||||
}) || loading;
|
||||
|
||||
const ownershipForDetails = useMemo(() => {
|
||||
const filteredOwnershipResult: {
|
||||
[key in EntityType]?: Array<SearchResult>;
|
||||
} = {};
|
||||
|
||||
Object.keys(ownershipResult).forEach((type) => {
|
||||
const entities = ownershipResult[type].data?.search?.searchResults;
|
||||
|
||||
if (entities && entities.length > 0) {
|
||||
filteredOwnershipResult[type] = ownershipResult[type].data?.search?.searchResults;
|
||||
}
|
||||
});
|
||||
return filteredOwnershipResult;
|
||||
}, [ownershipResult]);
|
||||
|
||||
if (error || (!loading && !error && !data)) {
|
||||
return <Alert type="error" message={error?.message || 'Entity failed to load'} />;
|
||||
}
|
||||
|
||||
const groupMemberRelationships = data?.corpUser?.relationships as EntityRelationshipsResult;
|
||||
|
||||
// Routed Tabs Constants
|
||||
const getTabs = () => {
|
||||
return [
|
||||
{
|
||||
name: TabType.Ownership,
|
||||
path: TabType.Ownership.toLocaleLowerCase(),
|
||||
content: <RelatedEntityResults searchResult={ownershipForDetails} />,
|
||||
name: TabType.Assets,
|
||||
path: TabType.Assets.toLocaleLowerCase(),
|
||||
content: <UserAssets />,
|
||||
display: {
|
||||
enabled: () => true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TabType.Groups,
|
||||
@ -75,36 +96,48 @@ export default function UserProfile() {
|
||||
content: (
|
||||
<UserGroups urn={urn} initialRelationships={groupMemberRelationships} pageSize={GROUP_PAGE_SIZE} />
|
||||
),
|
||||
display: {
|
||||
enabled: () => groupsDetails?.relationships.length > 0,
|
||||
},
|
||||
},
|
||||
].filter((tab) => ENABLED_TAB_TYPES.includes(tab.name));
|
||||
};
|
||||
const defaultTabPath = getTabs() && getTabs()?.length > 0 ? getTabs()[0].path : '';
|
||||
const onTabChange = () => null;
|
||||
|
||||
const getHeader = (user: CorpUser) => {
|
||||
const { editableInfo, info } = user;
|
||||
const displayName = entityRegistry.getDisplayName(EntityType.CorpUser, user);
|
||||
return (
|
||||
<UserHeader
|
||||
profileSrc={editableInfo?.pictureLink}
|
||||
name={displayName}
|
||||
title={info?.title}
|
||||
email={info?.email}
|
||||
skills={editableInfo?.skills}
|
||||
teams={editableInfo?.teams}
|
||||
/>
|
||||
);
|
||||
// Side bar data
|
||||
const sideBarData = {
|
||||
photoUrl: data?.corpUser?.editableProperties?.pictureLink || undefined,
|
||||
avatarName:
|
||||
data?.corpUser?.editableProperties?.displayName ||
|
||||
data?.corpUser?.info?.displayName ||
|
||||
data?.corpUser?.info?.fullName ||
|
||||
data?.corpUser?.urn,
|
||||
name: data?.corpUser?.editableProperties?.displayName || data?.corpUser?.info?.fullName || undefined,
|
||||
role: data?.corpUser?.editableProperties?.title || data?.corpUser?.info?.title || undefined,
|
||||
team: data?.corpUser?.editableProperties?.teams?.join(',') || undefined,
|
||||
email: data?.corpUser?.editableProperties?.email || data?.corpUser?.info?.email || undefined,
|
||||
slack: data?.corpUser?.editableProperties?.slack || undefined,
|
||||
phone: data?.corpUser?.editableProperties?.phone || undefined,
|
||||
aboutText: data?.corpUser?.editableProperties?.aboutMe || undefined,
|
||||
groupsDetails: data?.corpUser?.relationships as ExtendedEntityRelationshipsResult,
|
||||
urn,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{contentLoading && <Message type="loading" content="Loading..." style={messageStyle} />}
|
||||
{data && data.corpUser && (
|
||||
<LegacyEntityProfile
|
||||
title=""
|
||||
tags={null}
|
||||
header={getHeader(data.corpUser as CorpUser)}
|
||||
tabs={getTabs()}
|
||||
/>
|
||||
)}
|
||||
{contentLoading && <Message type="loading" content="Loading..." style={MESSAGE_STYLE} />}
|
||||
<UserProfileWrapper>
|
||||
<Row>
|
||||
<Col xl={5} lg={5} md={5} sm={24} xs={24}>
|
||||
<UserInfoSideBar sideBarData={sideBarData} refetch={refetch} />
|
||||
</Col>
|
||||
<Col xl={19} lg={19} md={19} sm={24} xs={24} style={{ borderLeft: '1px solid #E9E9E9' }}>
|
||||
<Content>
|
||||
<RoutedTabs defaultPath={defaultTabPath} tabs={getTabs()} onTabChange={onTabChange} />
|
||||
</Content>
|
||||
</Col>
|
||||
</Row>
|
||||
</UserProfileWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
14
datahub-web-react/src/app/entity/user/type.ts
Normal file
14
datahub-web-react/src/app/entity/user/type.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { EntityRelationshipsResult, EntityRelationship, Entity, CorpGroupProperties } from '../../../types.generated';
|
||||
|
||||
export interface ExtendedEntityRelationshipsResult extends EntityRelationshipsResult {
|
||||
relationships: Array<ExtendedEntityRelationship>;
|
||||
}
|
||||
|
||||
interface ExtendedEntityRelationship extends EntityRelationship {
|
||||
entity: ExtendedEntity;
|
||||
}
|
||||
|
||||
interface ExtendedEntity extends Entity {
|
||||
info: CorpGroupProperties;
|
||||
name: string;
|
||||
}
|
||||
@ -12,6 +12,9 @@ interface Props extends TabsProps {
|
||||
name: string;
|
||||
path: string;
|
||||
content: React.ReactNode;
|
||||
display?: {
|
||||
enabled: () => boolean;
|
||||
};
|
||||
}>;
|
||||
onTabChange?: (selectedTab: string) => void;
|
||||
}
|
||||
@ -39,9 +42,11 @@ export const RoutedTabs = ({ defaultPath, tabs, onTabChange, ...props }: Props)
|
||||
onChange={(newPath) => history.push(`${url}/${newPath}`)}
|
||||
{...props}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<TabPane tab={tab.name} key={tab.path.replace('/', '')} />
|
||||
))}
|
||||
{tabs.map((tab) => {
|
||||
return (
|
||||
<TabPane tab={tab.name} key={tab.path.replace('/', '')} disabled={!tab.display?.enabled()} />
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<Route exact path={path}>
|
||||
|
||||
@ -10,11 +10,18 @@ query getUser($urn: String!, $groupsCount: Int!) {
|
||||
lastName
|
||||
fullName
|
||||
email
|
||||
departmentName
|
||||
}
|
||||
editableInfo {
|
||||
editableProperties {
|
||||
slack
|
||||
phone
|
||||
pictureLink
|
||||
aboutMe
|
||||
teams
|
||||
skills
|
||||
displayName
|
||||
title
|
||||
email
|
||||
}
|
||||
globalTags {
|
||||
...globalTagsFields
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user