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

View File

@ -179,7 +179,7 @@ const DropDownList: FunctionComponent<DropDownListProp> = ({
</div> </div>
)} )}
{groupType === 'tab' && ( {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) => { {listGroups.map((grp, index) => {
return ( return (
<button <button

View File

@ -42,7 +42,15 @@ export interface Team {
*/ */
href: string; href: string;
id: string; id: string;
/**
* Can any user join this team during sign up? Value of true indicates yes, and false no.
*/
isJoinable?: boolean;
name: string; name: string;
/**
* Owner of this team.
*/
owner?: EntityReference;
/** /**
* List of entities owned by the team. * 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 * 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 * example, a table has an attribute called database of type EntityReference that captures
* the relationship of a table `belongs to a` database. * the relationship of a table `belongs to a` database.
*
* Owner of this team.
*/ */
export interface EntityReference { export interface EntityReference {
/**
* If true the entity referred to has been soft-deleted.
*/
deleted?: boolean;
/** /**
* Optional description of entity. * Optional description of entity.
*/ */

View File

@ -41,7 +41,11 @@ const jsonData = {
'fetch-table-details-error': 'Error while fetching table details!', 'fetch-table-details-error': 'Error while fetching table details!',
'fetch-table-queries-error': 'Error while fetching table queries!', 'fetch-table-queries-error': 'Error while fetching table queries!',
'fetch-tags-error': 'Error while fetching tags!', 'fetch-tags-error': 'Error while fetching tags!',
'update-owner-error': 'Error while updating owner',
'fetch-thread-error': 'Error while fetching threads!', 'fetch-thread-error': 'Error while fetching threads!',
'fetch-updated-conversation-error': 'fetch-updated-conversation-error':
'Error while fetching updated conversation!', 'Error while fetching updated conversation!',
'update-glossary-term-error': 'Error while updating glossary term!', '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>); return jest.fn().mockReturnValue(<div>Description</div>);
}); });
jest.mock('../../components/ManageTab/ManageTab.component', () => {
return jest.fn().mockReturnValue(<div>ManageTab</div>);
});
describe('Test Teams page', () => { describe('Test Teams page', () => {
it('Component should render', async () => { it('Component should render', async () => {
const { container } = render(<TeamsPage />); const { container } = render(<TeamsPage />);
@ -226,18 +230,20 @@ describe('Test Teams page', () => {
expect(await findByTestId(container, 'add-user-modal')).toBeInTheDocument(); 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 { container } = render(<TeamsPage />);
const tabs = await findByTestId(container, 'tabs'); const tabs = await findByTestId(container, 'tabs');
const user = await findByTestId(container, 'users'); const user = await findByTestId(container, 'users');
const asstes = await findByTestId(container, 'assets'); const asstes = await findByTestId(container, 'assets');
const roles = await findByTestId(container, 'roles'); 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(user).toBeInTheDocument();
expect(asstes).toBeInTheDocument(); expect(asstes).toBeInTheDocument();
expect(roles).toBeInTheDocument(); expect(roles).toBeInTheDocument();
expect(manage).toBeInTheDocument();
}); });
it('Description should be in document', async () => { it('Description should be in document', async () => {
@ -296,4 +302,19 @@ describe('Test Teams page', () => {
expect(confirmationModal).toBeInTheDocument(); 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 { compare } from 'fast-json-patch';
import { isUndefined, orderBy, toLower } from 'lodash'; import { isUndefined, orderBy, toLower } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { FormErrorData } from 'Models'; import { ExtraInfo, FormErrorData } from 'Models';
import React, { useEffect, useState } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import { Link, useHistory, useParams } from 'react-router-dom'; import { Link, useHistory, useParams } from 'react-router-dom';
import AppState from '../../AppState'; import AppState from '../../AppState';
import { useAuthContext } from '../../auth-provider/AuthProvider'; 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 PageContainerV1 from '../../components/containers/PageContainerV1';
import PageLayout from '../../components/containers/PageLayout'; import PageLayout from '../../components/containers/PageLayout';
import Loader from '../../components/Loader/Loader'; import Loader from '../../components/Loader/Loader';
import ManageTabComponent from '../../components/ManageTab/ManageTab.component';
import ConfirmationModal from '../../components/Modals/ConfirmationModal/ConfirmationModal'; import ConfirmationModal from '../../components/Modals/ConfirmationModal/ConfirmationModal';
import FormModal from '../../components/Modals/FormModal'; import FormModal from '../../components/Modals/FormModal';
import { import {
@ -56,8 +57,10 @@ import jsonData from '../../jsons/en';
import { import {
getActiveCatClass, getActiveCatClass,
getCountBadge, getCountBadge,
hasEditAccess,
isUrlFriendlyName, isUrlFriendlyName,
} from '../../utils/CommonUtils'; } from '../../utils/CommonUtils';
import { getInfoElements } from '../../utils/EntityUtils';
import AddUsersModal from './AddUsersModal'; import AddUsersModal from './AddUsersModal';
import Form from './Form'; import Form from './Form';
import UserCard from './UserCard'; 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 = () => { const fetchTeams = () => {
setIsLoading(true); setIsLoading(true);
getTeams(['users', 'owns', 'defaultRoles']) getTeams(['users', 'owns', 'defaultRoles', 'owner'])
.then((res: AxiosResponse) => { .then((res: AxiosResponse) => {
if (!team) { if (!team) {
setCurrentTeam(res.data.data[0]); setCurrentTeam(res.data.data[0]);
@ -127,7 +153,7 @@ const TeamsPage = () => {
const fetchCurrentTeam = (name: string, update = false) => { const fetchCurrentTeam = (name: string, update = false) => {
if (currentTeam?.name !== name || update) { if (currentTeam?.name !== name || update) {
setIsLoading(true); setIsLoading(true);
getTeamByName(name, ['users', 'owns', 'defaultRoles']) getTeamByName(name, ['users', 'owns', 'defaultRoles', 'owner'])
.then((res: AxiosResponse) => { .then((res: AxiosResponse) => {
setCurrentTeam(res.data); setCurrentTeam(res.data);
if (teams.length <= 0) { if (teams.length <= 0) {
@ -284,7 +310,7 @@ const TeamsPage = () => {
const getTabs = () => { const getTabs = () => {
return ( return (
<div className="tw-mb-3 "> <div className="tw-mb-3 tw-flex-initial">
<nav <nav
className="tw-flex tw-flex-row tw-gh-tabs-container" className="tw-flex tw-flex-row tw-gh-tabs-container"
data-testid="tabs"> data-testid="tabs">
@ -319,6 +345,14 @@ const TeamsPage = () => {
currentTab === 3 currentTab === 3
)} )}
</button> </button>
<button
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(4)}`}
data-testid="manage"
onClick={() => {
setCurrentTab(4);
}}>
Manage
</button>
</nav> </nav>
</div> </div>
); );
@ -531,6 +565,33 @@ const TeamsPage = () => {
return uniqueList; 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(() => { useEffect(() => {
setUserList(AppState.users); setUserList(AppState.users);
}, [AppState.users]); }, [AppState.users]);
@ -549,14 +610,17 @@ const TeamsPage = () => {
{error ? ( {error ? (
<ErrorPlaceHolder /> <ErrorPlaceHolder />
) : ( ) : (
<PageContainerV1 className="tw-py-4"> <PageContainerV1 className="tw-pt-4 tw-mb-4">
<PageLayout leftPanel={fetchLeftPanel()}> <PageLayout classes="tw-h-full" leftPanel={fetchLeftPanel()}>
{isLoading ? ( {isLoading ? (
<Loader /> <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 ? ( {teams.length > 0 ? (
<> <div className="tw-w-full tw-h-full tw-flex tw-flex-col">
<Fragment>
<div <div
className="tw-flex tw-justify-between tw-items-center" className="tw-flex tw-justify-between tw-items-center"
data-testid="header"> data-testid="header">
@ -568,17 +632,24 @@ const TeamsPage = () => {
<div> <div>
<NonAdminAction <NonAdminAction
html={ html={
<>You do not have permission to update the team.</> <Fragment>
You do not have permission to update the team.
</Fragment>
} }
isOwner={isOwner()}
permission={Operation.UpdateTeam} permission={Operation.UpdateTeam}
position="bottom"> position="bottom">
<Button <Button
className={classNames('tw-h-8 tw-rounded tw-mb-3', { className={classNames(
'tw-h-8 tw-rounded tw-mb-3',
{
'tw-opacity-40': 'tw-opacity-40':
!isAdminUser && !isAdminUser &&
!isAuthDisabled && !isAuthDisabled &&
!userPermissions[Operation.UpdateTeam], !userPermissions[Operation.UpdateTeam] &&
})} !isOwner(),
}
)}
data-testid="add-new-user-button" data-testid="add-new-user-button"
size="small" size="small"
theme="primary" theme="primary"
@ -589,15 +660,20 @@ const TeamsPage = () => {
</NonAdminAction> </NonAdminAction>
<NonAdminAction <NonAdminAction
html={ html={
<>You do not have permission to delete the team.</> <Fragment>
You do not have permission to delete the team.
</Fragment>
} }
isOwner={isOwner()}
position="bottom"> position="bottom">
<Button <Button
className={classNames( className={classNames(
'tw-h-8 tw-rounded tw-mb-3 tw-ml-2', 'tw-h-8 tw-rounded tw-mb-3 tw-ml-2',
{ {
'tw-opacity-40': 'tw-opacity-40':
!isAdminUser && !isAuthDisabled, !isAdminUser &&
!isAuthDisabled &&
!isOwner(),
} }
)} )}
data-testid="delete-team-button" data-testid="delete-team-button"
@ -610,6 +686,19 @@ const TeamsPage = () => {
</NonAdminAction> </NonAdminAction>
</div> </div>
</div> </div>
<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 <div
className="tw-mb-3 tw--ml-5" className="tw-mb-3 tw--ml-5"
data-testid="description-container"> data-testid="description-container">
@ -625,15 +714,29 @@ const TeamsPage = () => {
onDescriptionUpdate={onDescriptionUpdate} onDescriptionUpdate={onDescriptionUpdate}
/> />
</div> </div>
</Fragment>
<div className="tw-flex tw-flex-col tw-flex-grow">
{getTabs()} {getTabs()}
<div className="tw-flex-grow">
{currentTab === 1 && getUserCards()} {currentTab === 1 && getUserCards()}
{currentTab === 2 && getDatasetCards()} {currentTab === 2 && getDatasetCards()}
{currentTab === 3 && getDefaultRoles()} {currentTab === 3 && getDefaultRoles()}
{currentTab === 4 && (
<ManageTabComponent
hideTier
allowTeamOwner={false}
currentUser={currentTeam?.owner?.id}
hasEditAccess={isOwner()}
onSave={handleUpdateOwner}
/>
)}
</div>
</div>
{isAddingUsers && ( {isAddingUsers && (
<AddUsersModal <AddUsersModal
header={`Adding new users to ${ header={`Adding new users to ${
@ -644,7 +747,7 @@ const TeamsPage = () => {
onSave={(data) => createUsers(data)} onSave={(data) => createUsers(data)}
/> />
)} )}
</> </div>
) : ( ) : (
<ErrorPlaceHolder> <ErrorPlaceHolder>
<p className="tw-text-lg tw-text-center">No Teams Added.</p> <p className="tw-text-lg tw-text-center">No Teams Added.</p>

View File

@ -440,7 +440,7 @@ export const getInfoElements = (data: ExtraInfo) => {
<> <>
<span <span
className={classNames( 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, 'tw-w-52': (displayVal as string).length > 32,
} }