UI:Add owner support for team (#3591)

This commit is contained in:
Sachin Chaurasiya 2022-03-23 07:06:14 +05:30 committed by GitHub
parent d5a215636d
commit 90c1fecc44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 235 additions and 87 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { AxiosResponse } from 'axios';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
@ -30,7 +31,6 @@ import { CardWithListItems } from '../card-list/CardListItem/CardWithListItems.i
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import DropDownList from '../dropdown/DropDownList';
import Loader from '../Loader/Loader';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
type Props = {
currentTier?: string;
@ -41,6 +41,7 @@ type Props = {
tier: TableDetail['tier']
) => Promise<void>;
hasEditAccess: boolean;
allowTeamOwner?: boolean;
};
const ManageTab: FunctionComponent<Props> = ({
@ -49,6 +50,7 @@ const ManageTab: FunctionComponent<Props> = ({
hideTier = false,
onSave,
hasEditAccess,
allowTeamOwner = true,
}: Props) => {
const { userPermissions } = useAuth();
const { isAuthDisabled } = useAuthContext();
@ -121,6 +123,10 @@ const ManageTab: FunctionComponent<Props> = ({
return listOwners.find((item) => item.value === owner)?.name || '';
};
const getOwnerGroup = () => {
return allowTeamOwner ? ['Teams', 'Users'] : ['Users'];
};
const handleOwnerSelection = (
_e: React.MouseEvent<HTMLElement, MouseEvent>,
value?: string
@ -295,7 +301,7 @@ const ManageTab: FunctionComponent<Props> = ({
showSearchBar
dropDownList={listOwners}
groupType="tab"
listGroups={['Teams', 'Users']}
listGroups={getOwnerGroup()}
value={owner}
onSelect={handleOwnerSelection}
/>

View File

@ -179,7 +179,7 @@ const DropDownList: FunctionComponent<DropDownListProp> = ({
</div>
)}
{groupType === 'tab' && (
<div className="tw-flex tw-justify-around tw-border-b tw-border-separator tw-mb-1">
<div className="tw-flex tw-justify-between tw-border-b tw-border-separator tw-mb-1">
{listGroups.map((grp, index) => {
return (
<button

View File

@ -42,7 +42,15 @@ export interface Team {
*/
href: string;
id: string;
/**
* Can any user join this team during sign up? Value of true indicates yes, and false no.
*/
isJoinable?: boolean;
name: string;
/**
* Owner of this team.
*/
owner?: EntityReference;
/**
* List of entities owned by the team.
*/
@ -118,8 +126,14 @@ export interface FieldChange {
* EntityReference is used for capturing relationships from one entity to another. For
* example, a table has an attribute called database of type EntityReference that captures
* the relationship of a table `belongs to a` database.
*
* Owner of this team.
*/
export interface EntityReference {
/**
* If true the entity referred to has been soft-deleted.
*/
deleted?: boolean;
/**
* Optional description of entity.
*/

View File

@ -41,7 +41,11 @@ const jsonData = {
'fetch-table-details-error': 'Error while fetching table details!',
'fetch-table-queries-error': 'Error while fetching table queries!',
'fetch-tags-error': 'Error while fetching tags!',
'update-owner-error': 'Error while updating owner',
'fetch-thread-error': 'Error while fetching threads!',
'fetch-updated-conversation-error':
'Error while fetching updated conversation!',
'update-glossary-term-error': 'Error while updating glossary term!',

View File

@ -177,6 +177,10 @@ jest.mock('../../components/common/description/Description', () => {
return jest.fn().mockReturnValue(<div>Description</div>);
});
jest.mock('../../components/ManageTab/ManageTab.component', () => {
return jest.fn().mockReturnValue(<div>ManageTab</div>);
});
describe('Test Teams page', () => {
it('Component should render', async () => {
const { container } = render(<TeamsPage />);
@ -226,18 +230,20 @@ describe('Test Teams page', () => {
expect(await findByTestId(container, 'add-user-modal')).toBeInTheDocument();
});
it('Should have 3 tabs in the page', async () => {
it('Should have 4 tabs in the page', async () => {
const { container } = render(<TeamsPage />);
const tabs = await findByTestId(container, 'tabs');
const user = await findByTestId(container, 'users');
const asstes = await findByTestId(container, 'assets');
const roles = await findByTestId(container, 'roles');
const manage = await findByTestId(container, 'manage');
expect(tabs.childElementCount).toBe(3);
expect(tabs.childElementCount).toBe(4);
expect(user).toBeInTheDocument();
expect(asstes).toBeInTheDocument();
expect(roles).toBeInTheDocument();
expect(manage).toBeInTheDocument();
});
it('Description should be in document', async () => {
@ -296,4 +302,19 @@ describe('Test Teams page', () => {
expect(confirmationModal).toBeInTheDocument();
});
it('OnClick of manage tab, manage tab content should render', async () => {
const { container } = render(<TeamsPage />);
const assets = await findByTestId(container, 'manage');
fireEvent.click(
assets,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
})
);
expect(await findByText(container, /ManageTab/i)).toBeInTheDocument();
});
});

View File

@ -17,8 +17,8 @@ import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { isUndefined, orderBy, toLower } from 'lodash';
import { observer } from 'mobx-react';
import { FormErrorData } from 'Models';
import React, { useEffect, useState } from 'react';
import { ExtraInfo, FormErrorData } from 'Models';
import React, { Fragment, useEffect, useState } from 'react';
import { Link, useHistory, useParams } from 'react-router-dom';
import AppState from '../../AppState';
import { useAuthContext } from '../../auth-provider/AuthProvider';
@ -36,6 +36,7 @@ import NonAdminAction from '../../components/common/non-admin-action/NonAdminAct
import PageContainerV1 from '../../components/containers/PageContainerV1';
import PageLayout from '../../components/containers/PageLayout';
import Loader from '../../components/Loader/Loader';
import ManageTabComponent from '../../components/ManageTab/ManageTab.component';
import ConfirmationModal from '../../components/Modals/ConfirmationModal/ConfirmationModal';
import FormModal from '../../components/Modals/FormModal';
import {
@ -56,8 +57,10 @@ import jsonData from '../../jsons/en';
import {
getActiveCatClass,
getCountBadge,
hasEditAccess,
isUrlFriendlyName,
} from '../../utils/CommonUtils';
import { getInfoElements } from '../../utils/EntityUtils';
import AddUsersModal from './AddUsersModal';
import Form from './Form';
import UserCard from './UserCard';
@ -95,9 +98,32 @@ const TeamsPage = () => {
});
};
const extraInfo: Array<ExtraInfo> = [
{
key: 'Owner',
value:
currentTeam?.owner?.type === 'team'
? getTeamDetailsPath(
currentTeam?.owner?.displayName || currentTeam?.owner?.name || ''
)
: currentTeam?.owner?.displayName || currentTeam?.owner?.name || '',
placeholderText:
currentTeam?.owner?.displayName || currentTeam?.owner?.name || '',
isLink: currentTeam?.owner?.type === 'team',
openInNewTab: false,
},
];
const isOwner = () => {
return hasEditAccess(
currentTeam?.owner?.type || '',
currentTeam?.owner?.id || ''
);
};
const fetchTeams = () => {
setIsLoading(true);
getTeams(['users', 'owns', 'defaultRoles'])
getTeams(['users', 'owns', 'defaultRoles', 'owner'])
.then((res: AxiosResponse) => {
if (!team) {
setCurrentTeam(res.data.data[0]);
@ -127,7 +153,7 @@ const TeamsPage = () => {
const fetchCurrentTeam = (name: string, update = false) => {
if (currentTeam?.name !== name || update) {
setIsLoading(true);
getTeamByName(name, ['users', 'owns', 'defaultRoles'])
getTeamByName(name, ['users', 'owns', 'defaultRoles', 'owner'])
.then((res: AxiosResponse) => {
setCurrentTeam(res.data);
if (teams.length <= 0) {
@ -284,7 +310,7 @@ const TeamsPage = () => {
const getTabs = () => {
return (
<div className="tw-mb-3 ">
<div className="tw-mb-3 tw-flex-initial">
<nav
className="tw-flex tw-flex-row tw-gh-tabs-container"
data-testid="tabs">
@ -319,6 +345,14 @@ const TeamsPage = () => {
currentTab === 3
)}
</button>
<button
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(4)}`}
data-testid="manage"
onClick={() => {
setCurrentTab(4);
}}>
Manage
</button>
</nav>
</div>
);
@ -531,6 +565,33 @@ const TeamsPage = () => {
return uniqueList;
};
const handleUpdateOwner = (owner: Team['owner']) => {
const updatedTeam = {
...currentTeam,
owner,
};
const jsonPatch = compare(currentTeam as Team, updatedTeam);
return new Promise<void>((_, reject) => {
patchTeamDetail(currentTeam?.id, jsonPatch)
.then((res: AxiosResponse) => {
fetchCurrentTeam(res.data.name, true);
})
.catch((err: AxiosError) => {
reject();
const message = err.response?.data?.message;
showToast({
variant: 'error',
body:
message ??
`${jsonData['api-error-messages']['update-owner-error']} for ${
currentTeam?.displayName ?? currentTeam?.name
}`,
});
});
});
};
useEffect(() => {
setUserList(AppState.users);
}, [AppState.users]);
@ -549,90 +610,132 @@ const TeamsPage = () => {
{error ? (
<ErrorPlaceHolder />
) : (
<PageContainerV1 className="tw-py-4">
<PageLayout leftPanel={fetchLeftPanel()}>
<PageContainerV1 className="tw-pt-4 tw-mb-4">
<PageLayout classes="tw-h-full" leftPanel={fetchLeftPanel()}>
{isLoading ? (
<Loader />
) : (
<div className="tw-pb-3" data-testid="team-container">
<div
className="tw-pb-3 tw-w-full tw-h-full tw-flex tw-flex-col"
data-testid="team-container">
{teams.length > 0 ? (
<>
<div
className="tw-flex tw-justify-between tw-items-center"
data-testid="header">
<div className="tw-w-full tw-h-full tw-flex tw-flex-col">
<Fragment>
<div
className="tw-heading tw-text-link tw-text-base tw-truncate tw-w-52"
title={currentTeam?.displayName ?? currentTeam?.name}>
{currentTeam?.displayName ?? currentTeam?.name}
className="tw-flex tw-justify-between tw-items-center"
data-testid="header">
<div
className="tw-heading tw-text-link tw-text-base tw-truncate tw-w-52"
title={currentTeam?.displayName ?? currentTeam?.name}>
{currentTeam?.displayName ?? currentTeam?.name}
</div>
<div>
<NonAdminAction
html={
<Fragment>
You do not have permission to update the team.
</Fragment>
}
isOwner={isOwner()}
permission={Operation.UpdateTeam}
position="bottom">
<Button
className={classNames(
'tw-h-8 tw-rounded tw-mb-3',
{
'tw-opacity-40':
!isAdminUser &&
!isAuthDisabled &&
!userPermissions[Operation.UpdateTeam] &&
!isOwner(),
}
)}
data-testid="add-new-user-button"
size="small"
theme="primary"
variant="contained"
onClick={() => setIsAddingUsers(true)}>
Add new user
</Button>
</NonAdminAction>
<NonAdminAction
html={
<Fragment>
You do not have permission to delete the team.
</Fragment>
}
isOwner={isOwner()}
position="bottom">
<Button
className={classNames(
'tw-h-8 tw-rounded tw-mb-3 tw-ml-2',
{
'tw-opacity-40':
!isAdminUser &&
!isAuthDisabled &&
!isOwner(),
}
)}
data-testid="delete-team-button"
size="small"
theme="primary"
variant="contained"
onClick={() => deleteTeamHandler()}>
Delete Team
</Button>
</NonAdminAction>
</div>
</div>
<div>
<NonAdminAction
html={
<>You do not have permission to update the team.</>
<div className="tw-flex tw-gap-1 tw-mb-2 tw-flex-wrap">
{extraInfo.map((info, index) => (
<span className="tw-flex" key={index}>
{getInfoElements(info)}
{extraInfo.length !== 1 &&
index < extraInfo.length - 1 ? (
<span className="tw-mx-1.5 tw-inline-block tw-text-gray-400">
|
</span>
) : null}
</span>
))}
</div>
<div
className="tw-mb-3 tw--ml-5"
data-testid="description-container">
<Description
blurWithBodyBG
description={currentTeam?.description || ''}
entityName={
currentTeam?.displayName ?? currentTeam?.name
}
permission={Operation.UpdateTeam}
position="bottom">
<Button
className={classNames('tw-h-8 tw-rounded tw-mb-3', {
'tw-opacity-40':
!isAdminUser &&
!isAuthDisabled &&
!userPermissions[Operation.UpdateTeam],
})}
data-testid="add-new-user-button"
size="small"
theme="primary"
variant="contained"
onClick={() => setIsAddingUsers(true)}>
Add new user
</Button>
</NonAdminAction>
<NonAdminAction
html={
<>You do not have permission to delete the team.</>
}
position="bottom">
<Button
className={classNames(
'tw-h-8 tw-rounded tw-mb-3 tw-ml-2',
{
'tw-opacity-40':
!isAdminUser && !isAuthDisabled,
}
)}
data-testid="delete-team-button"
size="small"
theme="primary"
variant="contained"
onClick={() => deleteTeamHandler()}>
Delete Team
</Button>
</NonAdminAction>
isEdit={isEditable}
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
/>
</div>
</Fragment>
<div className="tw-flex tw-flex-col tw-flex-grow">
{getTabs()}
<div className="tw-flex-grow">
{currentTab === 1 && getUserCards()}
{currentTab === 2 && getDatasetCards()}
{currentTab === 3 && getDefaultRoles()}
{currentTab === 4 && (
<ManageTabComponent
hideTier
allowTeamOwner={false}
currentUser={currentTeam?.owner?.id}
hasEditAccess={isOwner()}
onSave={handleUpdateOwner}
/>
)}
</div>
</div>
<div
className="tw-mb-3 tw--ml-5"
data-testid="description-container">
<Description
blurWithBodyBG
description={currentTeam?.description || ''}
entityName={
currentTeam?.displayName ?? currentTeam?.name
}
isEdit={isEditable}
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
/>
</div>
{getTabs()}
{currentTab === 1 && getUserCards()}
{currentTab === 2 && getDatasetCards()}
{currentTab === 3 && getDefaultRoles()}
{isAddingUsers && (
<AddUsersModal
@ -644,7 +747,7 @@ const TeamsPage = () => {
onSave={(data) => createUsers(data)}
/>
)}
</>
</div>
) : (
<ErrorPlaceHolder>
<p className="tw-text-lg tw-text-center">No Teams Added.</p>

View File

@ -440,7 +440,7 @@ export const getInfoElements = (data: ExtraInfo) => {
<>
<span
className={classNames(
'tw-mr-1 tw-inline-block tw-truncate link-text',
'tw-mr-1 tw-inline-block tw-truncate link-text tw-align-middle',
{
'tw-w-52': (displayVal as string).length > 32,
}