Work on UI Feedback 0.10 (#4382)

This commit is contained in:
Shailesh Parmar 2022-04-23 01:38:24 +05:30 committed by GitHub
parent b5b42e1ca9
commit bca22514f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 209 additions and 209 deletions

View File

@ -17,10 +17,11 @@ import { compare } from 'fast-json-patch';
import { cloneDeep, orderBy } from 'lodash';
import { ExtraInfo, TableDetail } from 'Models';
import React, { Fragment, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { Link, useHistory } from 'react-router-dom';
import AppState from '../../AppState';
import {
getTeamAndUserDetailsPath,
getUserPath,
PAGE_SIZE_MEDIUM,
TITLE_FOR_NON_ADMIN_ACTION,
} from '../../constants/constants';
@ -45,6 +46,7 @@ import NextPrevious from '../common/next-previous/NextPrevious';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import Searchbar from '../common/searchbar/Searchbar';
import TabsPane from '../common/TabsPane/TabsPane';
import Loader from '../Loader/Loader';
import ManageTab from '../ManageTab/ManageTab.component';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import FormModal from '../Modals/FormModal';
@ -61,6 +63,7 @@ const TeamDetails = ({
isDescriptionEditable,
errorNewTeamData,
isAddingTeam,
isTeamMemberLoading,
handleAddTeam,
createNewTeam,
onNewTeamDataChange,
@ -76,6 +79,7 @@ const TeamDetails = ({
handleAddUser,
removeUserFromTeam,
}: TeamDetailsProp) => {
const history = useHistory();
const DELETE_USER_INITIAL_STATE = {
user: undefined,
state: false,
@ -127,7 +131,11 @@ const TeamDetails = ({
{
name: 'Manage',
isProtected: false,
isHidden: isOwner() || userPermissions[Operation.UpdateOwner],
isHidden: !(
hasAccess ||
isOwner() ||
userPermissions[Operation.UpdateOwner]
),
position: 4,
},
];
@ -218,6 +226,14 @@ const TeamDetails = ({
return Promise.reject();
};
/**
* Redirects user to profile page.
* @param name user name
*/
const handleUserRedirection = (name: string) => {
history.push(getUserPath(name));
};
useEffect(() => {
if (currentTeam) {
setHeading(currentTeam.displayName);
@ -284,64 +300,71 @@ const TeamDetails = ({
</div>
)}
</div>
{currentTeamUsers.length <= 0 ? (
<div className="tw-flex tw-flex-col tw-items-center tw-place-content-center tw-mt-40 tw-gap-1">
<p>
There are no users{' '}
{teamUsersSearchText
? `as ${teamUsersSearchText}.`
: `added yet.`}
</p>
{isActionAllowed(userPermissions[Operation.UpdateTeam]) ? (
<>
<p>Would like to start adding some?</p>
<Button
className="tw-h-8 tw-rounded tw-my-2"
size="small"
theme="primary"
variant="contained"
onClick={() => handleAddUser(true)}>
Add new user
</Button>
</>
) : null}
</div>
{isTeamMemberLoading ? (
<Loader />
) : (
<Fragment>
<div
className="tw-grid xxl:tw-grid-cols-4 lg:tw-grid-cols-3 md:tw-grid-cols-2 tw-gap-4"
data-testid="user-card-container">
{sortedUser.map((user, index) => {
const User = {
displayName: user.displayName || user.name,
fqn: user.name || '',
type: 'user',
id: user.id,
name: user.name,
};
<div>
{currentTeamUsers.length <= 0 ? (
<div className="tw-flex tw-flex-col tw-items-center tw-place-content-center tw-mt-40 tw-gap-1">
<p>
There are no users{' '}
{teamUsersSearchText
? `as ${teamUsersSearchText}.`
: `added yet.`}
</p>
{isActionAllowed(userPermissions[Operation.UpdateTeam]) ? (
<>
<p>Would like to start adding some?</p>
<Button
className="tw-h-8 tw-rounded tw-my-2"
size="small"
theme="primary"
variant="contained"
onClick={() => handleAddUser(true)}>
Add new user
</Button>
</>
) : null}
</div>
) : (
<Fragment>
<div
className="tw-grid xxl:tw-grid-cols-4 lg:tw-grid-cols-3 md:tw-grid-cols-2 tw-gap-4"
data-testid="user-card-container">
{sortedUser.map((user, index) => {
const User = {
displayName: user.displayName || user.name,
fqn: user.name || '',
type: 'user',
id: user.id,
name: user.name,
};
return (
<UserCard
isActionVisible
isIconVisible
item={User}
key={index}
onRemove={deleteUserHandler}
return (
<UserCard
isActionVisible
isIconVisible
item={User}
key={index}
onRemove={deleteUserHandler}
onTitleClick={handleUserRedirection}
/>
);
})}
</div>
{teamUserPagin.total > PAGE_SIZE_MEDIUM && (
<NextPrevious
currentPage={currentTeamUserPage}
isNumberBased={Boolean(teamUsersSearchText)}
pageSize={PAGE_SIZE_MEDIUM}
paging={teamUserPagin}
pagingHandler={teamUserPaginHandler}
totalCount={teamUserPagin.total}
/>
);
})}
</div>
{teamUserPagin.total > PAGE_SIZE_MEDIUM && (
<NextPrevious
currentPage={currentTeamUserPage}
isNumberBased={Boolean(teamUsersSearchText)}
pageSize={PAGE_SIZE_MEDIUM}
paging={teamUserPagin}
pagingHandler={teamUserPaginHandler}
totalCount={teamUserPagin.total}
/>
)}
</Fragment>
)}
</Fragment>
</div>
)}
</div>
);

View File

@ -18,20 +18,22 @@ import { TeamsAndUsersProps } from '../../interface/teamsAndUsers.interface';
import { getActiveCatClass, getCountBadge } from '../../utils/CommonUtils';
import { getActiveUsers } from '../../utils/TeamUtils';
import PageLayout from '../containers/PageLayout';
import Loader from '../Loader/Loader';
import TeamDetails from '../TeamDetails/TeamDetails';
import UserDetails from '../UserDetails/UserDetails';
const TeamsAndUsers = ({
users,
isUsersLoading,
admins,
bots,
activeUserTab,
userSearchTerm,
selectedUserList,
updateUser,
handleUserSearchTerm,
handleDeleteUser,
handleJoinTeamClick,
isRightPannelLoading,
hasAccess,
isTeamVisible,
teams,
@ -54,6 +56,7 @@ const TeamsAndUsers = ({
teamUserPaginHandler,
changeCurrentTeam,
isAddingUsers,
isTeamMemberLoading,
getUniqueUserList,
addUsersToTeam,
handleAddUser,
@ -145,47 +148,52 @@ const TeamsAndUsers = ({
return (
<PageLayout classes="tw-h-full tw-p-4" leftPanel={fetchLeftPanel()}>
<div
className="tw-pb-3 tw-w-full tw-h-full tw-flex tw-flex-col"
data-testid="team-and-user-container">
{!isTeamVisible ? (
<UserDetails
handleAddNewUser={handleAddNewUser}
handleDeleteUser={handleDeleteUser}
handleUserSearchTerm={handleUserSearchTerm}
selectedUserList={selectedUserList}
updateUser={updateUser}
userSearchTerm={userSearchTerm}
/>
) : (
<TeamDetails
addUsersToTeam={addUsersToTeam}
createNewTeam={createNewTeam}
currentTeam={currentTeam}
currentTeamUserPage={currentTeamUserPage}
currentTeamUsers={currentTeamUsers}
descriptionHandler={descriptionHandler}
errorNewTeamData={errorNewTeamData}
getUniqueUserList={getUniqueUserList}
handleAddTeam={handleAddTeam}
handleAddUser={handleAddUser}
handleJoinTeamClick={handleJoinTeamClick}
handleTeamUsersSearchAction={handleTeamUsersSearchAction}
hasAccess={hasAccess}
isAddingTeam={isAddingTeam}
isAddingUsers={isAddingUsers}
isDescriptionEditable={isDescriptionEditable}
removeUserFromTeam={removeUserFromTeam}
teamUserPagin={teamUserPagin}
teamUserPaginHandler={teamUserPaginHandler}
teamUsersSearchText={teamUsersSearchText}
teams={teams}
updateTeamHandler={updateTeamHandler}
onDescriptionUpdate={onDescriptionUpdate}
onNewTeamDataChange={onNewTeamDataChange}
/>
)}
</div>
{isRightPannelLoading ? (
<Loader />
) : (
<div
className="tw-pb-3 tw-w-full tw-h-full tw-flex tw-flex-col"
data-testid="team-and-user-container">
{!isTeamVisible ? (
<UserDetails
handleAddNewUser={handleAddNewUser}
handleDeleteUser={handleDeleteUser}
handleUserSearchTerm={handleUserSearchTerm}
isUsersLoading={isUsersLoading}
selectedUserList={selectedUserList}
userSearchTerm={userSearchTerm}
/>
) : (
<TeamDetails
addUsersToTeam={addUsersToTeam}
createNewTeam={createNewTeam}
currentTeam={currentTeam}
currentTeamUserPage={currentTeamUserPage}
currentTeamUsers={currentTeamUsers}
descriptionHandler={descriptionHandler}
errorNewTeamData={errorNewTeamData}
getUniqueUserList={getUniqueUserList}
handleAddTeam={handleAddTeam}
handleAddUser={handleAddUser}
handleJoinTeamClick={handleJoinTeamClick}
handleTeamUsersSearchAction={handleTeamUsersSearchAction}
hasAccess={hasAccess}
isAddingTeam={isAddingTeam}
isAddingUsers={isAddingUsers}
isDescriptionEditable={isDescriptionEditable}
isTeamMemberLoading={isTeamMemberLoading}
removeUserFromTeam={removeUserFromTeam}
teamUserPagin={teamUserPagin}
teamUserPaginHandler={teamUserPaginHandler}
teamUsersSearchText={teamUsersSearchText}
teams={teams}
updateTeamHandler={updateTeamHandler}
onDescriptionUpdate={onDescriptionUpdate}
onNewTeamDataChange={onNewTeamDataChange}
/>
)}
</div>
)}
</PageLayout>
);
};

View File

@ -23,18 +23,19 @@ type Item = {
name: string;
id?: string;
email: string;
isActiveUser: boolean;
profilePhoto: string;
teamCount: string;
isActiveUser?: boolean;
profilePhoto?: string;
teamCount?: string;
};
type Props = {
item: Item;
onClick: (value: string) => void;
showTeams?: boolean;
onClick?: (value: string) => void;
onDelete?: (id: string, name: string) => void;
};
const UserDataCard = ({ item, onClick, onDelete }: Props) => {
const UserDataCard = ({ item, onClick, onDelete, showTeams = true }: Props) => {
return (
<div
className="tw-card tw-flex tw-justify-between tw-py-2 tw-px-3 tw-group"
@ -58,21 +59,21 @@ const UserDataCard = ({ item, onClick, onDelete }: Props) => {
<div className="tw-flex tw-justify-between">
<p
className={classNames('tw-font-normal', {
'tw-cursor-pointer': Boolean(onClick),
'tw-cursor-pointer hover:tw-underline': Boolean(onClick),
})}
onClick={() => {
onClick(item.id as string);
onClick?.(item.name);
}}>
{item.description}
</p>
{!item.isActiveUser && (
{!item?.isActiveUser && (
<span className="tw-text-xs tw-bg-badge tw-border tw-px-2 tw-py-0.5 tw-rounded">
Inactive
</span>
)}
</div>
<p className="tw-truncate">{item.email}</p>
<p>Teams: {item.teamCount}</p>
{showTeams && <p>Teams: {item.teamCount}</p>}
</div>
</div>
{!isNil(onDelete) && (

View File

@ -11,27 +11,28 @@
* limitations under the License.
*/
import { compare, Operation } from 'fast-json-patch';
import { isUndefined } from 'lodash';
import React, { useEffect, useState } from 'react';
import AppState from '../../AppState';
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
import { Role } from '../../generated/entity/teams/role';
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
getUserPath,
TITLE_FOR_NON_ADMIN_ACTION,
} from '../../constants/constants';
import { EntityReference, User } from '../../generated/entity/teams/user';
import { getEntityName } from '../../utils/CommonUtils';
import { Button } from '../buttons/Button/Button';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import Searchbar from '../common/searchbar/Searchbar';
import Loader from '../Loader/Loader';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import UserDetailsModal from '../Modals/UserDetailsModal/UserDetailsModal';
import UserDataCard from '../UserDataCard/UserDataCard';
type UserDetailsProps = {
selectedUserList: User[];
handleUserSearchTerm: (value: string) => void;
userSearchTerm: string;
updateUser: (id: string, data: Operation[], updatedUser: User) => void;
isUsersLoading: boolean;
handleDeleteUser: (id: string) => void;
handleAddNewUser: () => void;
};
@ -44,45 +45,14 @@ interface DeleteUserInfo {
const UserDetails = ({
selectedUserList,
userSearchTerm,
isUsersLoading,
handleDeleteUser,
handleUserSearchTerm,
updateUser,
handleAddNewUser,
}: UserDetailsProps) => {
const [selectedUser, setSelectedUser] = useState<User>();
const [roles, setRoles] = useState<Role[]>([]);
const history = useHistory();
const [deletingUser, setDeletingUser] = useState<DeleteUserInfo>();
const selectUser = (id: string) => {
const user = selectedUserList.find((user) => user.id === id);
if (user) {
setSelectedUser(user);
} else {
setSelectedUser(undefined);
}
};
const handleSave = (rolesData: Array<string>) => {
if (selectedUser) {
const updatedData: User = {
...selectedUser,
isAdmin: Boolean(rolesData.find((role) => role === 'admin')),
roles: roles
.filter((role) => rolesData.includes(role.id))
.map((role) => ({
id: role.id,
type: 'role',
href: role.href,
displayName: role.displayName,
})),
};
const jsonPatch = compare(selectedUser, updatedData);
updateUser(selectedUser.id, jsonPatch, updatedData);
setSelectedUser(undefined);
}
};
const handleDeleteUserModal = (id: string, name: string) => {
setDeletingUser({
name,
@ -90,18 +60,24 @@ const UserDetails = ({
});
};
/**
* Redirects user to profile page.
* @param name user name
*/
const handleUserRedirection = (name: string) => {
history.push(getUserPath(name));
};
const onConfirmDeleteUser = (id: string) => {
handleDeleteUser(id);
setDeletingUser(undefined);
};
useEffect(() => {
setRoles(AppState.userRoles);
}, [AppState.userRoles]);
const getUserCards = () => {
return (
<>
return isUsersLoading ? (
<Loader />
) : (
<div>
{selectedUserList.length > 0 ? (
<div
className="tw-grid xxl:tw-grid-cols-3 lg:tw-grid-cols-2 tw-gap-4"
@ -123,13 +99,10 @@ const UserDetails = ({
};
return (
<div
className="tw-cursor-pointer"
key={index}
onClick={() => selectUser(User.id)}>
<div key={index}>
<UserDataCard
item={User}
onClick={selectUser}
onClick={handleUserRedirection}
onDelete={handleDeleteUserModal}
/>
</div>
@ -141,7 +114,7 @@ const UserDetails = ({
<p>No user available</p>
</ErrorPlaceHolder>
)}
</>
</div>
);
};
@ -173,16 +146,6 @@ const UserDetails = ({
</div>
{getUserCards()}
{!isUndefined(selectedUser) && (
<UserDetailsModal
header="Update user"
roles={roles}
userData={selectedUser}
onCancel={() => setSelectedUser(undefined)}
onSave={handleSave}
/>
)}
{!isUndefined(deletingUser) && (
<ConfirmationModal
bodyText={`Are you sure you want to delete ${deletingUser.name}?`}

View File

@ -28,6 +28,8 @@ export type TeamDeleteType = {
export interface TeamsAndUsersProps {
hasAccess: boolean;
isUsersLoading: boolean;
isTeamMemberLoading: boolean;
isTeamVisible: boolean;
activeUserTab: UserType | undefined;
activeUserTabHandler: (value: UserType | undefined) => void;
@ -42,9 +44,9 @@ export interface TeamsAndUsersProps {
currentTeamUserPage: number;
teamUsersSearchText: string;
isDescriptionEditable: boolean;
isRightPannelLoading: boolean;
errorNewTeamData: FormErrorData | undefined;
isAddingTeam: boolean;
updateUser: (id: string, data: Operation[], updatedUser: User) => void;
createNewTeam: (data: Team) => void;
handleAddTeam: (value: boolean) => void;
onNewTeamDataChange: (
@ -82,6 +84,7 @@ export interface TeamDetailsProp {
currentTeamUserPage: number;
teamUsersSearchText: string;
isDescriptionEditable: boolean;
isTeamMemberLoading: boolean;
hasAccess: boolean;
errorNewTeamData: FormErrorData | undefined;
isAddingTeam: boolean;

View File

@ -61,7 +61,10 @@ const TeamsAndUsersPage = () => {
const { isAuthDisabled } = useAuthContext();
const history = useHistory();
const [isLoading, setIsLoading] = useState(true);
const [isRightPannelLoading, setIsRightPannelLoading] = useState(true);
const [isTeamMemberLoading, setIsTeamMemberLoading] = useState(true);
const [isTeamVisible, setIsTeamVisible] = useState(true);
const [isUsersLoading, setIsUsersLoading] = useState(true);
const [teams, setTeams] = useState<Team[]>([]);
const [currentTeam, setCurrentTeam] = useState<Team>();
const [currentTeamUsers, setCurrentTeamUsers] = useState<User[]>([]);
@ -84,6 +87,10 @@ const TeamsAndUsersPage = () => {
setIsDescriptionEditable(value);
};
const handleRightPannelLoading = (value: boolean) => {
setIsRightPannelLoading(value);
};
const handleAddTeam = (value: boolean) => {
setIsAddingTeam(value);
};
@ -101,6 +108,7 @@ const TeamsAndUsersPage = () => {
};
const setAllTabList = (users: User[], type = '') => {
setIsUsersLoading(true);
const dBots = users.filter((user) => user.isBot);
const dUsers = users.filter((user) => !user.isBot);
const dAdmins = users.filter((user) => user.isAdmin);
@ -126,9 +134,11 @@ const TeamsAndUsersPage = () => {
break;
}
}
setIsUsersLoading(false);
};
const handleUserSearchTerm = (value: string) => {
setIsUsersLoading(true);
setUserSearchTerm(value);
if (value) {
let updatedList: User[] = [];
@ -160,42 +170,15 @@ const TeamsAndUsersPage = () => {
} else {
setAllTabList(userList, activeUserTab);
}
setIsUsersLoading(false);
};
const handleAddNewUser = () => {
history.push(ROUTES.CREATE_USER);
};
const updateUser = (id: string, data: Operation[], updatedUser: User) => {
setIsLoading(true);
updateUserDetail(id, data)
.then((res) => {
if (res.data) {
const updatedData = (userList || []).map((user) => {
if (user.id === id) {
return updatedUser;
}
return user;
});
setAllTabList(updatedData, activeUserTab);
setUserList(updatedData);
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['update-user-error']
);
})
.finally(() => {
setIsLoading(false);
});
};
const handleDeleteUser = (id: string) => {
setIsLoading(true);
setIsUsersLoading(true);
deleteUser(id)
.then(() => {
AppState.updateUsers((userList || []).filter((item) => item.id !== id));
@ -208,6 +191,7 @@ const TeamsAndUsersPage = () => {
})
.finally(() => {
setIsLoading(false);
setIsUsersLoading(false);
});
};
@ -218,21 +202,25 @@ const TeamsAndUsersPage = () => {
team: string,
pagin = {} as { [key: string]: string }
) => {
getUsers('', PAGE_SIZE_MEDIUM, { team, ...pagin }).then(
(res: AxiosResponse) => {
setIsTeamMemberLoading(true);
getUsers('', PAGE_SIZE_MEDIUM, { team, ...pagin })
.then((res: AxiosResponse) => {
if (res.data) {
setCurrentTeamUsers(res.data.data);
setTeamUserPagin(res.data.paging);
}
}
);
})
.catch(() => {
setCurrentTeamUsers([]);
setTeamUserPagin({ total: 0 });
})
.finally(() => setIsTeamMemberLoading(false));
};
/**
* Make API call to fetch all the teams
*/
const fetchTeams = () => {
setIsLoading(true);
getTeams(['users', 'owns', 'defaultRoles', 'owner'])
.then((res: AxiosResponse) => {
if (res.data) {
@ -255,6 +243,7 @@ const TeamsAndUsersPage = () => {
})
.finally(() => {
setIsLoading(false);
handleRightPannelLoading(false);
});
};
@ -263,7 +252,6 @@ const TeamsAndUsersPage = () => {
*/
const fetchCurrentTeam = (name: string, update = false) => {
if (currentTeam?.name !== name || update) {
// setIsLoading(true);
getTeamByName(name, ['users', 'owns', 'defaultRoles', 'owner'])
.then((res: AxiosResponse) => {
if (res.data) {
@ -299,6 +287,7 @@ const TeamsAndUsersPage = () => {
};
const searchUsers = (text: string, currentPage: number) => {
setIsTeamMemberLoading(true);
searchData(
text,
currentPage,
@ -317,7 +306,8 @@ const TeamsAndUsersPage = () => {
})
.catch(() => {
setCurrentTeamUsers([]);
});
})
.finally(() => setIsTeamMemberLoading(false));
};
const teamUserPaginHandler = (cursorValue: string | number) => {
@ -418,6 +408,7 @@ const TeamsAndUsersPage = () => {
* @param name - team name
*/
const changeCurrentTeam = (name: string, isUsersCategory: boolean) => {
handleRightPannelLoading(true);
history.push(getTeamAndUserDetailsPath(name));
if (isUsersCategory) {
setIsTeamVisible(false);
@ -635,7 +626,10 @@ const TeamsAndUsersPage = () => {
isAddingTeam={isAddingTeam}
isAddingUsers={isAddingUsers}
isDescriptionEditable={isDescriptionEditable}
isRightPannelLoading={isRightPannelLoading}
isTeamMemberLoading={isTeamMemberLoading}
isTeamVisible={isTeamVisible}
isUsersLoading={isUsersLoading}
removeUserFromTeam={removeUserFromTeam}
selectedUserList={selectedUserList}
teamUserPagin={teamUserPagin}
@ -643,7 +637,6 @@ const TeamsAndUsersPage = () => {
teamUsersSearchText={teamUsersSearchText}
teams={teams}
updateTeamHandler={updateTeamHandler}
updateUser={updateUser}
userSearchTerm={userSearchTerm}
users={users}
onDescriptionUpdate={onDescriptionUpdate}

View File

@ -43,6 +43,7 @@ interface Props {
isDataset?: boolean;
isCheckBoxes?: boolean;
isOwner?: boolean;
onTitleClick?: (value: string) => void;
onSelect?: (value: string) => void;
onRemove?: (value: string) => void;
}
@ -54,6 +55,7 @@ const UserCard = ({
isDataset = false,
isCheckBoxes = false,
isOwner = false,
onTitleClick,
onSelect,
onRemove,
}: Props) => {
@ -172,9 +174,16 @@ const UserCard = ({
<p
className={classNames(
'tw-font-normal',
isActionVisible ? 'tw-truncate tw-w-32' : null
isActionVisible ? 'tw-truncate tw-w-32' : null,
{
'tw-cursor-pointer hover:tw-underline':
Boolean(onTitleClick),
}
)}
title={item.displayName}>
title={item.displayName}
onClick={() => {
onTitleClick?.(item.fqn);
}}>
{item.displayName}
</p>
{item.name && (