Fix #4292: Added hard delete support for team & users page (#4342)

This commit is contained in:
Shailesh Parmar 2022-04-22 11:42:32 +05:30 committed by GitHub
parent db0e34c709
commit fa0d386bd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 295 additions and 340 deletions

View File

@ -123,17 +123,24 @@ export const getInitialEntity: Function = (): Promise<AxiosResponse> => {
export const deleteEntity: Function = (
entityType: string,
entityId: string,
isRecursive: boolean
isRecursive: boolean,
isSoftDelete = false
): Promise<AxiosResponse> => {
const searchParams = new URLSearchParams({ hardDelete: `true` });
if (!isUndefined(isRecursive)) {
searchParams.set('recursive', `${isRecursive}`);
let path = '';
if (isSoftDelete) {
path = getURLWithQueryFields(`/${entityType}/${entityId}`);
} else {
const searchParams = new URLSearchParams({ hardDelete: `true` });
if (!isUndefined(isRecursive)) {
searchParams.set('recursive', `${isRecursive}`);
}
path = getURLWithQueryFields(
`/${entityType}/${entityId}`,
'',
`${searchParams.toString()}`
);
}
const path = getURLWithQueryFields(
`/${entityType}/${entityId}`,
'',
`${searchParams.toString()}`
);
return APIClient.delete(path);
};

View File

@ -18,7 +18,7 @@ import React, { RefObject, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { getTeamDetailsPath } from '../../constants/constants';
import { getTeamAndUserDetailsPath } from '../../constants/constants';
import { observerOptions } from '../../constants/Mydata.constants';
import { EntityType } from '../../enums/entity.enum';
import { Dashboard } from '../../generated/entity/data/dashboard';
@ -200,7 +200,7 @@ const DashboardDetails = ({
key: 'Owner',
value:
owner?.type === 'team'
? getTeamDetailsPath(owner?.name || '')
? getTeamAndUserDetailsPath(owner?.name || '')
: getEntityName(owner),
placeholderText: getEntityPlaceHolder(
getEntityName(owner),

View File

@ -17,7 +17,7 @@ import { ColumnJoins, EntityTags, ExtraInfo } from 'Models';
import React, { RefObject, useEffect, useState } from 'react';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { getTeamDetailsPath, ROUTES } from '../../constants/constants';
import { getTeamAndUserDetailsPath, ROUTES } from '../../constants/constants';
import { observerOptions } from '../../constants/Mydata.constants';
import { CSMode } from '../../enums/codemirror.enum';
import { EntityType, FqnPart } from '../../enums/entity.enum';
@ -329,7 +329,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
key: 'Owner',
value:
owner?.type === 'team'
? getTeamDetailsPath(owner?.name || '')
? getTeamAndUserDetailsPath(owner?.name || '')
: getEntityName(owner),
placeholderText: getEntityPlaceHolder(
getEntityName(owner),

View File

@ -0,0 +1,52 @@
import React from 'react';
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
import { Button } from '../buttons/Button/Button';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
type DeleteWidgetBodyProps = {
header: string;
description: string;
buttonText: string;
isOwner?: boolean;
hasPermission: boolean;
onClick: () => void;
};
const DeleteWidgetBody = ({
header,
description,
buttonText,
isOwner,
hasPermission,
onClick,
}: DeleteWidgetBodyProps) => {
return (
<div className="tw-flex tw-justify-between tw-px-4 tw-py-2">
<div data-testid="danger-zone-text">
<h4 className="tw-text-base" data-testid="danger-zone-text-title">
{header}
</h4>
<p data-testid="danger-zone-text-para">{description}</p>
</div>
<NonAdminAction
className="tw-self-center"
html={<p>{TITLE_FOR_NON_ADMIN_ACTION}</p>}
isOwner={isOwner}
position="left">
<Button
className="tw-px-2 tw-py-1 tw-rounded tw-h-auto tw-self-center tw-shadow"
data-testid="delete-button"
disabled={!hasPermission}
size="custom"
theme="primary"
type="button"
variant="outlined"
onClick={onClick}>
{buttonText}
</Button>
</NonAdminAction>
</div>
);
};
export default DeleteWidgetBody;

View File

@ -24,7 +24,6 @@ import { useAuthContext } from '../../authentication/auth-provider/AuthProvider'
import { deleteEntity } from '../../axiosAPIs/miscAPI';
import { getCategory } from '../../axiosAPIs/tagAPI';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
import { ENTITY_DELETE_STATE } from '../../constants/entity.constants';
import { EntityType } from '../../enums/entity.enum';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
@ -37,9 +36,11 @@ import { Button } from '../buttons/Button/Button';
import CardListItem from '../card-list/CardListItem/CardWithListItems';
import { CardWithListItems } from '../card-list/CardListItem/CardWithListItems.interface';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import ToggleSwitchV1 from '../common/toggle-switch/ToggleSwitchV1';
import DropDownList from '../dropdown/DropDownList';
import Loader from '../Loader/Loader';
import EntityDeleteModal from '../Modals/EntityDeleteModal/EntityDeleteModal';
import DeleteWidgetBody from './DeleteWidgetBody';
import { ManageProps, Status } from './ManageTab.interface';
const ManageTab: FunctionComponent<ManageProps> = ({
@ -54,8 +55,10 @@ const ManageTab: FunctionComponent<ManageProps> = ({
entityName,
entityType,
entityId,
allowSoftDelete,
isRecursiveDelete,
deletEntityMessage,
handleIsJoinable,
}: ManageProps) => {
const history = useHistory();
const { userPermissions, isAdminUser } = useAuth();
@ -161,8 +164,8 @@ const ManageTab: FunctionComponent<ManageProps> = ({
setActiveTier(cardId);
};
const handleOnEntityDelete = () => {
setEntityDeleteState((prev) => ({ ...prev, state: true }));
const handleOnEntityDelete = (softDelete = false) => {
setEntityDeleteState((prev) => ({ ...prev, state: true, softDelete }));
};
const handleOnEntityDeleteCancel = () => {
@ -190,7 +193,12 @@ const ManageTab: FunctionComponent<ManageProps> = ({
const handleOnEntityDeleteConfirm = () => {
setEntityDeleteState((prev) => ({ ...prev, loading: 'waiting' }));
deleteEntity(prepareEntityType(), entityId, isRecursiveDelete)
deleteEntity(
prepareEntityType(),
entityId,
isRecursiveDelete,
allowSoftDelete
)
.then((res: AxiosResponse) => {
if (res.status === 200) {
setTimeout(() => {
@ -227,6 +235,7 @@ const ManageTab: FunctionComponent<ManageProps> = ({
entityName={entityName as string}
entityType={entityType as string}
loadingState={entityDeleteState.loading}
softDelete={entityDeleteState.softDelete}
onCancel={handleOnEntityDeleteCancel}
onConfirm={handleOnEntityDeleteConfirm}
/>
@ -240,34 +249,28 @@ const ManageTab: FunctionComponent<ManageProps> = ({
return allowDelete && entityId && entityName && entityType ? (
<div className="tw-mt-1" data-testid="danger-zone">
<hr className="tw-border-main tw-mb-4" />
<div className="tw-border tw-border-error tw-px-4 tw-py-2 tw-flex tw-justify-between tw-rounded tw-mt-3 tw-shadow">
<div data-testid="danger-zone-text">
<h4 className="tw-text-base" data-testid="danger-zone-text-title">
Delete {entityType} {entityName}
</h4>
<p data-testid="danger-zone-text-para">{prepareDeleteMessage()}</p>
</div>
<NonAdminAction
className="tw-self-center"
html={
<Fragment>
<p>{TITLE_FOR_NON_ADMIN_ACTION}</p>
</Fragment>
}
<div className="tw-border tw-border-error tw-rounded tw-mt-3 tw-shadow">
{allowSoftDelete && (
<div className="tw-border-b tw-border-error">
<DeleteWidgetBody
buttonText={`Soft delete this ${entityType}`}
description={prepareDeleteMessage()}
hasPermission={isAdminUser || isAuthDisabled}
header={`Soft delete ${entityType} ${entityName}`}
isOwner={isAdminUser}
onClick={() => handleOnEntityDelete(true)}
/>
</div>
)}
<DeleteWidgetBody
buttonText={`Delete this ${entityType}`}
description={prepareDeleteMessage()}
hasPermission={isAdminUser || isAuthDisabled}
header={`Delete ${entityType} ${entityName}`}
isOwner={isAdminUser}
position="left">
<Button
className="tw-px-2 tw-py-1 tw-rounded tw-h-auto tw-self-center tw-shadow"
data-testid="delete-button"
disabled={!isAdminUser && !isAuthDisabled}
size="custom"
theme="primary"
type="button"
variant="outlined"
onClick={handleOnEntityDelete}>
Delete this {entityType}
</Button>
</NonAdminAction>
onClick={handleOnEntityDelete}
/>
</div>
</div>
) : null;
@ -315,18 +318,18 @@ const ManageTab: FunctionComponent<ManageProps> = ({
userPermissions[Operation.UpdateTeam] ||
!hasEditAccess;
const joinableSwitch = isActionAllowed ? (
<div className="tw-flex">
<label htmlFor="join-team">Allow users to join this team</label>
<div
className={classNames('toggle-switch ', { open: teamJoinable })}
data-testid="team-isJoinable-switch"
id="join-team"
onClick={() => setTeamJoinable((prev) => !prev)}>
<div className="switch" />
const joinableSwitch =
isActionAllowed && !isUndefined(teamJoinable) ? (
<div className="tw-flex">
<label htmlFor="join-team">Open to join</label>
<ToggleSwitchV1
checked={teamJoinable}
handleCheck={() => {
handleIsJoinable?.(!teamJoinable);
}}
/>
</div>
</div>
) : null;
) : null;
return !isUndefined(isJoinable) ? (
<div className="tw-mt-3 tw-mb-1">{joinableSwitch}</div>

View File

@ -18,11 +18,13 @@ export interface ManageProps {
currentUser?: string;
hideTier?: boolean;
isJoinable?: boolean;
allowSoftDelete?: boolean;
onSave: (
owner: TableDetail['owner'],
tier: TableDetail['tier'],
isJoinable?: boolean
) => Promise<void>;
handleIsJoinable?: (bool: boolean) => void;
hasEditAccess: boolean;
allowTeamOwner?: boolean;
entityId?: string;

View File

@ -14,7 +14,7 @@
import {
findAllByTestId,
findByTestId,
fireEvent,
findByText,
render,
} from '@testing-library/react';
import React from 'react';
@ -41,6 +41,10 @@ jest.mock('../../hooks/authHooks', () => ({
}),
}));
jest.mock('../common/toggle-switch/ToggleSwitchV1', () => {
return jest.fn().mockImplementation(() => <p>ToggleSwitchV1.Component</p>);
});
const mockTierData = {
children: [
{
@ -96,23 +100,11 @@ describe('Test Manage tab Component', () => {
<ManageTab hasEditAccess isJoinable onSave={mockFunction} />
);
const isJoinableSwitch = await findByTestId(
const isJoinableSwitch = await findByText(
container,
'team-isJoinable-switch'
'ToggleSwitchV1.Component'
);
expect(isJoinableSwitch).toHaveClass('open');
fireEvent.click(
isJoinableSwitch,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
})
);
expect(isJoinableSwitch).not.toHaveClass('open');
expect(isJoinableSwitch).toBeInTheDocument();
});

View File

@ -29,6 +29,7 @@ interface Prop extends HTMLAttributes<HTMLDivElement> {
entityType: string;
loadingState: string;
bodyText?: string;
softDelete?: boolean;
}
const EntityDeleteModal: FC<Prop> = ({
@ -39,6 +40,7 @@ const EntityDeleteModal: FC<Prop> = ({
onCancel,
onConfirm,
bodyText,
softDelete = false,
}: Prop) => {
const [name, setName] = useState('');
@ -58,7 +60,15 @@ const EntityDeleteModal: FC<Prop> = ({
<div className="tw-modal-container tw-w-120">
<div className={classNames('tw-modal-header')}>
<p className="tw-modal-title" data-testid="modal-header">
Delete <strong>{entityName}</strong>
{softDelete ? (
<span>
Soft delete <strong>{entityName}</strong>
</span>
) : (
<span>
Delete <strong>{entityName}</strong>
</span>
)}
</p>
</div>
<div className={classNames('tw-modal-body')} data-testid="body-text">

View File

@ -19,7 +19,7 @@ import React, { Fragment, RefObject, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { getTeamDetailsPath } from '../../constants/constants';
import { getTeamAndUserDetailsPath } from '../../constants/constants';
import { observerOptions } from '../../constants/Mydata.constants';
import { EntityType } from '../../enums/entity.enum';
import { Pipeline, Task } from '../../generated/entity/data/pipeline';
@ -207,7 +207,7 @@ const PipelineDetails = ({
key: 'Owner',
value:
owner?.type === 'team'
? getTeamDetailsPath(owner?.name || '')
? getTeamAndUserDetailsPath(owner?.name || '')
: getEntityName(owner),
placeholderText: getEntityPlaceHolder(
getEntityName(owner),

View File

@ -15,13 +15,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { cloneDeep, orderBy } from 'lodash';
import { ExtraInfo } from 'Models';
import { ExtraInfo, TableDetail } from 'Models';
import React, { Fragment, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import AppState from '../../AppState';
import {
getTeamAndUserDetailsPath,
PAGE_SIZE_12,
PAGE_SIZE_MEDIUM,
TITLE_FOR_NON_ADMIN_ACTION,
} from '../../constants/constants';
import { OwnerType } from '../../enums/user.enum';
@ -37,7 +37,6 @@ import AddUsersModal from '../../pages/teams/AddUsersModal';
import UserCard from '../../pages/teams/UserCard';
import { hasEditAccess } from '../../utils/CommonUtils';
import { getInfoElements } from '../../utils/EntityUtils';
import { getOwnerList } from '../../utils/ManageUtils';
import SVGIcons from '../../utils/SvgUtils';
import { Button } from '../buttons/Button/Button';
import Description from '../common/description/Description';
@ -46,8 +45,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 ToggleSwitchV1 from '../common/toggle-switch/ToggleSwitchV1';
import DropDownList from '../dropdown/DropDownList';
import ManageTab from '../ManageTab/ManageTab.component';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import FormModal from '../Modals/FormModal';
import Form from './Form';
@ -63,9 +61,6 @@ const TeamDetails = ({
isDescriptionEditable,
errorNewTeamData,
isAddingTeam,
deletingTeam,
deleteTeamById,
handleDeleteTeam,
handleAddTeam,
createNewTeam,
onNewTeamDataChange,
@ -81,8 +76,12 @@ const TeamDetails = ({
handleAddUser,
removeUserFromTeam,
}: TeamDetailsProp) => {
const DELETE_USER_INITIAL_STATE = {
user: undefined,
state: false,
leave: false,
};
const { userPermissions } = useAuth();
const [showOwnerDropdown, setShowOwnerDropdown] = useState(false);
const [currentTab, setCurrentTab] = useState(1);
const [isHeadingEditing, setIsHeadingEditing] = useState(false);
const [currentUser, setCurrentUser] = useState<User>();
@ -92,7 +91,19 @@ const TeamDetails = ({
const [deletingUser, setDeletingUser] = useState<{
user: UserTeams | undefined;
state: boolean;
}>({ user: undefined, state: false });
leave: boolean;
}>(DELETE_USER_INITIAL_STATE);
/**
* Check if current team is the owner or not
* @returns - True true or false based on hasEditAccess response
*/
const isOwner = () => {
return hasEditAccess(
currentTeam?.owner?.type || '',
currentTeam?.owner?.id || ''
);
};
const tabs = [
{
@ -113,6 +124,12 @@ const TeamDetails = ({
position: 3,
count: currentTeam?.defaultRoles?.length,
},
{
name: 'Manage',
isProtected: false,
isHidden: isOwner() || userPermissions[Operation.UpdateOwner],
position: 4,
},
];
const extraInfo: ExtraInfo = {
@ -129,44 +146,15 @@ const TeamDetails = ({
openInNewTab: false,
};
/**
* Check if current team is the owner or not
* @returns - True true or false based on hasEditAccess response
*/
const isOwner = () => {
return hasEditAccess(
currentTeam?.owner?.type || '',
currentTeam?.owner?.id || ''
);
};
const isActionAllowed = (operation = false) => {
return hasAccess || isOwner() || operation;
};
const handleOwnerSelection = (
_e: React.MouseEvent<HTMLElement, MouseEvent>,
value?: string
) => {
if (value && currentTeam) {
const updatedData: Team = {
...currentTeam,
owner: {
id: value,
type: OwnerType.USER,
},
};
updateTeamHandler(updatedData);
}
setShowOwnerDropdown(false);
};
const handleOpenToJoinToggle = () => {
const handleOpenToJoinToggle = (value: boolean) => {
if (currentTeam) {
const updatedData: Team = {
...currentTeam,
isJoinable: !currentTeam.isJoinable,
isJoinable: value,
};
updateTeamHandler(updatedData);
}
@ -174,7 +162,7 @@ const TeamDetails = ({
const handleRemoveUser = () => {
removeUserFromTeam(deletingUser.user?.id as string).then(() => {
setDeletingUser({ user: undefined, state: false });
setDeletingUser(DELETE_USER_INITIAL_STATE);
});
};
@ -217,6 +205,19 @@ const TeamDetails = ({
}
};
const handleManageSave = (owner: TableDetail['owner']) => {
if (currentTeam) {
const updatedData: Team = {
...currentTeam,
owner: owner,
};
return updateTeamHandler(updatedData);
}
return Promise.reject();
};
useEffect(() => {
if (currentTeam) {
setHeading(currentTeam.displayName);
@ -228,23 +229,19 @@ const TeamDetails = ({
* Take user id as input to find out the user data and set it for delete
* @param id - user id
*/
const deleteUserHandler = (id: string) => {
const deleteUserHandler = (id: string, leave = false) => {
const user = [...(currentTeam?.users as Array<UserTeams>)].find(
(u) => u.id === id
);
setDeletingUser({ user, state: true });
setDeletingUser({ user, state: true, leave });
};
const getJoinableWidget = () => {
return isActionAllowed(userPermissions[Operation.UpdateTeam]) ? (
<div className="tw-flex">
<label htmlFor="join-team">Open to join</label>
<ToggleSwitchV1
checked={(currentTeam?.isJoinable as boolean) || false}
handleCheck={handleOpenToJoinToggle}
/>
</div>
) : null;
const removeUserBodyText = (leave: boolean) => {
const text = leave
? `leave the team ${currentTeam?.displayName ?? currentTeam?.name}?`
: `remove ${deletingUser.user?.displayName ?? deletingUser.user?.name}?`;
return `Are you sure you want to ${text}`;
};
/**
@ -334,11 +331,11 @@ const TeamDetails = ({
);
})}
</div>
{teamUserPagin.total > PAGE_SIZE_12 && (
{teamUserPagin.total > PAGE_SIZE_MEDIUM && (
<NextPrevious
currentPage={currentTeamUserPage}
isNumberBased={Boolean(teamUsersSearchText)}
pageSize={PAGE_SIZE_12}
pageSize={PAGE_SIZE_MEDIUM}
paging={teamUserPagin}
pagingHandler={teamUserPaginHandler}
totalCount={teamUserPagin.total}
@ -504,7 +501,7 @@ const TeamDetails = ({
data-testid="header">
{getTeamHeading()}
<div className="tw-flex">
{isActionAllowed() ? (
{isActionAllowed() && (
<Fragment>
<NonAdminAction
position="bottom"
@ -514,81 +511,45 @@ const TeamDetails = ({
data-testid="add-teams"
size="small"
theme="primary"
variant="contained"
variant="outlined"
onClick={() => {
handleAddTeam(true);
}}>
Create New Team
</Button>
</NonAdminAction>
<NonAdminAction
html={
<Fragment>
You do not have permission to delete the team.
</Fragment>
}
isOwner={isOwner()}
position="bottom">
<Button
className="tw-h-8 tw-rounded tw-ml-2"
data-testid="delete-team-button"
size="small"
theme="primary"
variant="outlined"
onClick={() =>
handleDeleteTeam({ team: currentTeam, state: true })
}>
Delete Team
</Button>
</NonAdminAction>
</Fragment>
) : !isAlreadyJoinedTeam(currentTeam.id) &&
)}
{!isAlreadyJoinedTeam(currentTeam.id) ? (
currentTeam.isJoinable ? (
<Button
className="tw-h-8 tw-px-2 tw-mb-4 tw-ml-2"
data-testid="join-teams"
size="small"
theme="primary"
variant="contained"
onClick={joinTeam}>
Join Team
</Button>
) : null
) : (
<Button
className="tw-h-8 tw-px-2 tw-mb-4"
data-testid="join-teams"
className="tw-h-8 tw-rounded tw-ml-2"
data-testid="delete-team-button"
size="small"
theme="primary"
variant="contained"
onClick={joinTeam}>
Join Team
variant="outlined"
onClick={() =>
currentUser && deleteUserHandler(currentUser.id, true)
}>
Leave Team
</Button>
) : null}
)}
</div>
</div>
<div className="tw-flex tw-items-center tw-gap-1 tw-mb-2">
<span>{getInfoElements(extraInfo)}</span>
{isActionAllowed(userPermissions[Operation.UpdateOwner]) && (
<span className="tw-relative">
<Button
className="tw-relative tw-pb-1"
data-testid="owner-dropdown"
size="custom"
theme="primary"
variant="link"
onClick={() => setShowOwnerDropdown((visible) => !visible)}>
<SVGIcons
alt="edit"
className="tw-ml-1"
icon="icon-edit"
title="Edit"
width="12px"
/>
</Button>
{showOwnerDropdown && (
<DropDownList
dropDownList={getOwnerList()}
groupType="tab"
listGroups={['Users']}
value={currentTeam?.owner?.id}
onSelect={handleOwnerSelection}
/>
)}
</span>
)}
</div>
<div>{getJoinableWidget()}</div>
<div className="tw-mb-3 tw--ml-5" data-testid="description-container">
<Description
blurWithBodyBG
@ -617,6 +578,25 @@ const TeamDetails = ({
{currentTab === 2 && getDatasetCards()}
{currentTab === 3 && getDefaultRoles()}
{currentTab === 4 && (
<div>
<ManageTab
allowDelete
allowSoftDelete
hasEditAccess
hideTier
allowTeamOwner={false}
currentUser={currentTeam.owner?.id}
entityId={currentTeam.id}
entityName={currentTeam.displayName || currentTeam.name}
entityType="team"
handleIsJoinable={handleOpenToJoinToggle}
isJoinable={currentTeam.isJoinable}
onSave={handleManageSave}
/>
</div>
)}
</div>
</div>
</Fragment>
@ -657,30 +637,13 @@ const TeamDetails = ({
/>
)}
{deletingTeam.state && (
<ConfirmationModal
bodyText={`Are you sure you want to delete the team "${
deletingTeam.team?.displayName || deletingTeam.team?.name
}"?`}
cancelText="Cancel"
confirmText="Confirm"
header="Delete Team"
onCancel={() => handleDeleteTeam({ team: undefined, state: false })}
onConfirm={() => {
deleteTeamById(deletingTeam.team?.id as string);
}}
/>
)}
{deletingUser.state && (
<ConfirmationModal
bodyText={`Are you sure you want to remove ${
deletingUser.user?.displayName ?? deletingUser.user?.name
}?`}
bodyText={removeUserBodyText(deletingUser.leave)}
cancelText="Cancel"
confirmText="Confirm"
header="Removing user"
onCancel={() => setDeletingUser({ user: undefined, state: false })}
header={deletingUser.leave ? 'Leave team' : 'Removing user'}
onCancel={() => setDeletingUser(DELETE_USER_INITIAL_STATE)}
onConfirm={handleRemoveUser}
/>
)}

View File

@ -45,9 +45,6 @@ const TeamsAndUsers = ({
isAddingTeam,
createNewTeam,
handleAddNewUser,
deletingTeam,
deleteTeamById,
handleDeleteTeam,
handleAddTeam,
onNewTeamDataChange,
updateTeamHandler,
@ -167,14 +164,11 @@ const TeamsAndUsers = ({
currentTeam={currentTeam}
currentTeamUserPage={currentTeamUserPage}
currentTeamUsers={currentTeamUsers}
deleteTeamById={deleteTeamById}
deletingTeam={deletingTeam}
descriptionHandler={descriptionHandler}
errorNewTeamData={errorNewTeamData}
getUniqueUserList={getUniqueUserList}
handleAddTeam={handleAddTeam}
handleAddUser={handleAddUser}
handleDeleteTeam={handleDeleteTeam}
handleJoinTeamClick={handleJoinTeamClick}
handleTeamUsersSearchAction={handleTeamUsersSearchAction}
hasAccess={hasAccess}

View File

@ -15,7 +15,7 @@ import { EntityTags } from 'Models';
import React, { RefObject, useEffect, useState } from 'react';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { getTeamDetailsPath } from '../../constants/constants';
import { getTeamAndUserDetailsPath } from '../../constants/constants';
import { observerOptions } from '../../constants/Mydata.constants';
import { EntityType } from '../../enums/entity.enum';
import { Topic } from '../../generated/entity/data/topic';
@ -198,7 +198,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
key: 'Owner',
value:
owner?.type === 'team'
? getTeamDetailsPath(owner?.name || '')
? getTeamAndUserDetailsPath(owner?.name || '')
: getEntityName(owner),
placeholderText: getEntityPlaceHolder(
getEntityName(owner),

View File

@ -22,7 +22,7 @@ import appState from '../../AppState';
import { getVersion } from '../../axiosAPIs/miscAPI';
import {
getExplorePathWithSearch,
getTeamDetailsPath,
getTeamAndUserDetailsPath,
navLinkSettings,
ROUTES,
} from '../../constants/constants';
@ -166,7 +166,7 @@ const Appbar: React.FC = (): JSX.Element => {
<span className="tw-font-medium tw-text-xs">Teams</span>
{teams?.map((t, i) => (
<p key={i}>
<Link to={getTeamDetailsPath(t.name as string)}>
<Link to={getTeamAndUserDetailsPath(t.name as string)}>
{t.displayName}
</Link>
</p>

View File

@ -23,7 +23,7 @@ import { useAuthContext } from '../../authentication/auth-provider/AuthProvider'
import { getVersion } from '../../axiosAPIs/miscAPI';
import {
getExplorePathWithSearch,
getTeamDetailsPath,
getTeamAndUserDetailsPath,
getUserPath,
navLinkSettings,
ROUTES,
@ -181,7 +181,7 @@ const Appbar: React.FC = (): JSX.Element => {
<span className="tw-font-medium tw-text-xs">Teams</span>
{teams.map((t, i) => (
<p key={i}>
<Link to={getTeamDetailsPath(t.name as string)}>
<Link to={getTeamAndUserDetailsPath(t.name as string)}>
{t.displayName}
</Link>
</p>

View File

@ -17,7 +17,7 @@ import { FQN_SEPARATOR_CHAR } from './char.constants';
export const FOLLOWERS_VIEW_CAP = 20;
export const JSON_TAB_SIZE = 2;
export const PAGE_SIZE = 10;
export const PAGE_SIZE_12 = 12;
export const PAGE_SIZE_MEDIUM = 16;
export const API_RES_MAX_SIZE = 100000;
export const LIST_SIZE = 5;
export const SIDEBAR_WIDTH_COLLAPSED = 290;
@ -50,7 +50,6 @@ const PLACEHOLDER_ROUTE_SERVICE_FQN = ':serviceFQN';
export const PLACEHOLDER_ROUTE_SERVICE_CAT = ':serviceCategory';
const PLACEHOLDER_ROUTE_SEARCHQUERY = ':searchQuery';
const PLACEHOLDER_ROUTE_TAB = ':tab';
const PLACEHOLDER_ROUTE_TEAM = ':team';
const PLACEHOLDER_ROUTE_TEAM_AND_USER = ':teamAndUser';
const PLAEHOLDER_ROUTE_VERSION = ':version';
const PLACEHOLDER_ROUTE_ENTITY_TYPE = ':entityType';
@ -151,8 +150,6 @@ export const ROUTES = {
EXPLORE_WITH_TAB: `/explore/${PLACEHOLDER_ROUTE_TAB}`,
WORKFLOWS: '/workflows',
SQL_BUILDER: '/sql-builder',
TEAMS: '/teams',
TEAM_DETAILS: `/teams/${PLACEHOLDER_ROUTE_TEAM}`,
TEAMS_AND_USERS: '/teams-and-users',
TEAMS_AND_USERS_DETAILS: `/teams-and-users/${PLACEHOLDER_ROUTE_TEAM_AND_USER}`,
SETTINGS: '/settings',
@ -325,16 +322,6 @@ export const getTeamAndUserDetailsPath = (name?: string) => {
return path;
};
export const getTeamDetailsPath = (teamName?: string) => {
let path = ROUTES.TEAMS;
if (teamName) {
path = ROUTES.TEAM_DETAILS;
path = path.replace(PLACEHOLDER_ROUTE_TEAM, teamName);
}
return path;
};
export const getEditWebhookPath = (webhookName: string) => {
let path = ROUTES.EDIT_WEBHOOK;
path = path.replace(PLACEHOLDER_WEBHOOK_NAME, webhookName);
@ -406,13 +393,7 @@ export const navLinkSettings = [
{ name: 'Services', to: '/services', disabled: false },
{ name: 'Tags', to: '/tags', disabled: false },
{ name: 'Teams & Users', to: ROUTES.TEAMS_AND_USERS, disabled: false },
// { name: 'Teams', to: '/teams', disabled: false },
// { name: 'Users', to: '/user-list', disabled: false, isAdminOnly: true },
// { name: 'Store', to: '/store', disabled: false },
{ name: 'Webhooks', to: '/webhooks', disabled: false },
// { name: 'Ingestions', to: '/ingestion', disabled: false },
// { name: 'Marketplace', to: '/marketplace', disabled: true },
// { name: 'Preferences', to: '/preference', disabled: true },
];
export const TITLE_FOR_NON_OWNER_ACTION =

View File

@ -14,4 +14,5 @@
export const ENTITY_DELETE_STATE = {
loading: 'initial',
state: false,
softDelete: false,
};

View File

@ -47,16 +47,13 @@ export interface TeamsAndUsersProps {
updateUser: (id: string, data: Operation[], updatedUser: User) => void;
createNewTeam: (data: Team) => void;
handleAddTeam: (value: boolean) => void;
handleDeleteTeam: (data: TeamDeleteType) => void;
deleteTeamById: (id: string) => void;
deletingTeam: TeamDeleteType;
onNewTeamDataChange: (
data: Team,
forceSet?: boolean
) => {
[key: string]: string;
};
updateTeamHandler: (data: Team) => void;
updateTeamHandler: (data: Team) => Promise<void>;
handleDeleteUser: (id: string) => void;
handleTeamUsersSearchAction: (text: string) => void;
teamUserPaginHandler: (
@ -88,9 +85,6 @@ export interface TeamDetailsProp {
hasAccess: boolean;
errorNewTeamData: FormErrorData | undefined;
isAddingTeam: boolean;
handleDeleteTeam: (data: TeamDeleteType) => void;
deleteTeamById: (id: string) => void;
deletingTeam: TeamDeleteType;
handleAddTeam: (value: boolean) => void;
onNewTeamDataChange: (
data: Team,
@ -101,7 +95,7 @@ export interface TeamDetailsProp {
descriptionHandler: (value: boolean) => void;
onDescriptionUpdate: (value: string) => void;
handleTeamUsersSearchAction: (text: string) => void;
updateTeamHandler: (data: Team) => void;
updateTeamHandler: (data: Team) => Promise<void>;
createNewTeam: (data: Team) => void;
teamUserPaginHandler: (
cursorValue: string | number,

View File

@ -59,7 +59,7 @@ import {
getDatabaseSchemaDetailsPath,
getServiceDetailsPath,
getTableDetailsPath,
getTeamDetailsPath,
getTeamAndUserDetailsPath,
} from '../../constants/constants';
import { observerOptions } from '../../constants/Mydata.constants';
import { EntityType, FqnPart, TabSpecificField } from '../../enums/entity.enum';
@ -174,7 +174,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
key: 'Owner',
value:
databaseSchema?.owner?.type === 'team'
? getTeamDetailsPath(
? getTeamAndUserDetailsPath(
databaseSchema?.owner?.displayName ||
databaseSchema?.owner?.name ||
''

View File

@ -23,7 +23,6 @@ import { useAuthContext } from '../../authentication/auth-provider/AuthProvider'
import { searchData } from '../../axiosAPIs/miscAPI';
import {
createTeam,
deleteTeam,
getTeamByName,
getTeams,
patchTeamDetail,
@ -38,7 +37,7 @@ import Loader from '../../components/Loader/Loader';
import TeamsAndUsers from '../../components/TeamsAndUsers/TeamsAndUsers.component';
import {
getTeamAndUserDetailsPath,
PAGE_SIZE_12,
PAGE_SIZE_MEDIUM,
ROUTES,
} from '../../constants/constants';
import { SearchIndex } from '../../enums/search.enum';
@ -50,7 +49,6 @@ import {
} from '../../generated/entity/teams/user';
import { Paging } from '../../generated/type/paging';
import { useAuth } from '../../hooks/authHooks';
import { TeamDeleteType } from '../../interface/teamsAndUsers.interface';
import jsonData from '../../jsons/en';
import { formatUsersResponse } from '../../utils/APIUtils';
import { isUrlFriendlyName } from '../../utils/CommonUtils';
@ -74,10 +72,6 @@ const TeamsAndUsersPage = () => {
const [isAddingTeam, setIsAddingTeam] = useState<boolean>(false);
const [isAddingUsers, setIsAddingUsers] = useState<boolean>(false);
const [errorNewTeamData, setErrorNewTeamData] = useState<FormErrorData>();
const [deletingTeam, setDeletingTeam] = useState<TeamDeleteType>({
team: undefined,
state: false,
});
const [activeUserTab, setactiveUserTab] = useState<UserType>();
const [userList, setUserList] = useState<Array<User>>([]);
const [users, setUsers] = useState<Array<User>>([]);
@ -94,10 +88,6 @@ const TeamsAndUsersPage = () => {
setIsAddingTeam(value);
};
const handleDeleteTeam = (data: TeamDeleteType) => {
setDeletingTeam(data);
};
const handleAddUser = (data: boolean) => {
setIsAddingUsers(data);
};
@ -228,7 +218,7 @@ const TeamsAndUsersPage = () => {
team: string,
pagin = {} as { [key: string]: string }
) => {
getUsers('', PAGE_SIZE_12, { team, ...pagin }).then(
getUsers('', PAGE_SIZE_MEDIUM, { team, ...pagin }).then(
(res: AxiosResponse) => {
if (res.data) {
setCurrentTeamUsers(res.data.data);
@ -273,7 +263,7 @@ const TeamsAndUsersPage = () => {
*/
const fetchCurrentTeam = (name: string, update = false) => {
if (currentTeam?.name !== name || update) {
setIsLoading(true);
// setIsLoading(true);
getTeamByName(name, ['users', 'owns', 'defaultRoles', 'owner'])
.then((res: AxiosResponse) => {
if (res.data) {
@ -308,43 +298,11 @@ const TeamsAndUsersPage = () => {
}
};
const goToTeams = () => {
if (teamAndUser) {
history.push(getTeamAndUserDetailsPath());
} else {
fetchTeams();
}
};
/**
* Take team id and delete the team
* @param id - Team id
*/
const deleteTeamById = (id: string) => {
deleteTeam(id)
.then((res: AxiosResponse) => {
if (res.data) {
goToTeams();
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['delete-team-error']
);
})
.finally(() => {
setDeletingTeam({ team: undefined, state: false });
});
};
const searchUsers = (text: string, currentPage: number) => {
searchData(
text,
currentPage,
PAGE_SIZE_12,
PAGE_SIZE_MEDIUM,
`(teams:${currentTeam?.id})`,
'',
'',
@ -472,20 +430,27 @@ const TeamsAndUsersPage = () => {
const updateTeamHandler = (updatedData: Team) => {
const jsonPatch = compare(currentTeam as Team, updatedData);
patchTeamDetail(currentTeam?.id, jsonPatch)
.then((res: AxiosResponse) => {
if (res.data) {
fetchCurrentTeam(res.data.name, true);
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((error: AxiosError) => {
showErrorToast(
error,
jsonData['api-error-messages']['update-team-error']
);
});
return new Promise<void>((resolve, reject) => {
patchTeamDetail(currentTeam?.id, jsonPatch)
.then((res: AxiosResponse) => {
if (res.data) {
fetchCurrentTeam(res.data.name, true);
resolve();
} else {
reject();
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((error: AxiosError) => {
showErrorToast(
error,
jsonData['api-error-messages']['update-team-error']
);
reject();
});
});
};
/**
@ -560,8 +525,7 @@ const TeamsAndUsersPage = () => {
* @param id - user id
*/
const removeUserFromTeam = (id: string) => {
const users = [...(currentTeam?.users as Array<UserTeams>)];
const newUsers = users.filter((user) => {
const newUsers = currentTeam?.users?.filter((user) => {
return user.id !== id;
});
const updatedTeam = {
@ -657,15 +621,12 @@ const TeamsAndUsersPage = () => {
currentTeam={currentTeam}
currentTeamUserPage={currentTeamUserPage}
currentTeamUsers={currentTeamUsers}
deleteTeamById={deleteTeamById}
deletingTeam={deletingTeam}
descriptionHandler={descriptionHandler}
errorNewTeamData={errorNewTeamData}
getUniqueUserList={getUniqueUserList}
handleAddNewUser={handleAddNewUser}
handleAddTeam={handleAddTeam}
handleAddUser={handleAddUser}
handleDeleteTeam={handleDeleteTeam}
handleDeleteUser={handleDeleteUser}
handleJoinTeamClick={handleJoinTeamClick}
handleTeamUsersSearchAction={handleTeamUsersSearchAction}

View File

@ -58,7 +58,7 @@ import {
getDatabaseSchemaDetailsPath,
getExplorePathWithSearch,
getServiceDetailsPath,
getTeamDetailsPath,
getTeamAndUserDetailsPath,
PAGE_SIZE,
pagingObject,
} from '../../constants/constants';
@ -182,7 +182,7 @@ const DatabaseDetails: FunctionComponent = () => {
key: 'Owner',
value:
database?.owner?.type === 'team'
? getTeamDetailsPath(
? getTeamAndUserDetailsPath(
database?.owner?.displayName || database?.owner?.name || ''
)
: database?.owner?.displayName || database?.owner?.name || '',

View File

@ -48,7 +48,7 @@ import ServiceConfig from '../../components/ServiceConfig/ServiceConfig';
import TagsViewer from '../../components/tags-viewer/tags-viewer';
import {
getServiceDetailsPath,
getTeamDetailsPath,
getTeamAndUserDetailsPath,
PAGE_SIZE,
pagingObject,
} from '../../constants/constants';
@ -186,7 +186,7 @@ const ServicePage: FunctionComponent = () => {
key: 'Owner',
value:
serviceDetails?.owner?.type === 'team'
? getTeamDetailsPath(serviceDetails?.owner?.name || '')
? getTeamAndUserDetailsPath(serviceDetails?.owner?.name || '')
: serviceDetails?.owner?.name || '',
placeholderText: serviceDetails?.owner?.displayName || '',
isLink: serviceDetails?.owner?.type === 'team',

View File

@ -40,7 +40,7 @@ import ManageTabComponent from '../../components/ManageTab/ManageTab.component';
import ConfirmationModal from '../../components/Modals/ConfirmationModal/ConfirmationModal';
import FormModal from '../../components/Modals/FormModal';
import {
getTeamDetailsPath,
getTeamAndUserDetailsPath,
TITLE_FOR_NON_ADMIN_ACTION,
} from '../../constants/constants';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
@ -95,7 +95,7 @@ const TeamsPage = () => {
key: 'Owner',
value:
currentTeam?.owner?.type === 'team'
? getTeamDetailsPath(
? getTeamAndUserDetailsPath(
currentTeam?.owner?.displayName || currentTeam?.owner?.name || ''
)
: currentTeam?.owner?.displayName || currentTeam?.owner?.name || '',
@ -149,7 +149,7 @@ const TeamsPage = () => {
const goToTeams = () => {
if (team) {
history.push(getTeamDetailsPath());
history.push(getTeamAndUserDetailsPath());
} else {
fetchTeams();
setCurrentTab(1);
@ -375,7 +375,7 @@ const TeamsPage = () => {
* @param name - team name
*/
const changeCurrentTeam = (name: string) => {
history.push(getTeamDetailsPath(name));
history.push(getTeamAndUserDetailsPath(name));
};
const Tabs = () => {

View File

@ -37,11 +37,9 @@ import ServicesPage from '../pages/services';
import SignupPage from '../pages/signup';
import SwaggerPage from '../pages/swagger';
import TagsPage from '../pages/tags';
import TeamsPage from '../pages/teams';
import TeamsAndUsersPage from '../pages/TeamsAndUsersPage/TeamsAndUsersPage.component';
import TopicDetailsPage from '../pages/TopicDetails/TopicDetailsPage.component';
import TourPageComponent from '../pages/tour-page/TourPage.component';
import UserListPage from '../pages/UserListPage/UserListPage';
import UserPage from '../pages/UserPage/UserPage.component';
import WebhooksPage from '../pages/WebhooksPage/WebhooksPage.component';
import AdminProtectedRoute from './AdminProtectedRoute';
@ -53,8 +51,6 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
<Route exact component={ExplorePage} path={ROUTES.EXPLORE} />
<Route component={ExplorePage} path={ROUTES.EXPLORE_WITH_SEARCH} />
<Route component={ExplorePage} path={ROUTES.EXPLORE_WITH_TAB} />
<Route exact component={TeamsPage} path={ROUTES.TEAMS} />
<Route exact component={TeamsPage} path={ROUTES.TEAM_DETAILS} />
<Route
exact
component={TeamsAndUsersPage}
@ -155,11 +151,6 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
component={CreateUserPage}
path={ROUTES.CREATE_USER}
/>
<AdminProtectedRoute
exact
component={UserListPage}
path={ROUTES.USER_LIST}
/>
<Redirect to={ROUTES.NOT_FOUND} />
</Switch>
);

View File

@ -445,8 +445,9 @@
/* react tostify */
.Toastify__toast-container {
@apply tw-font-sans tw-break-all;
@apply tw-font-sans;
width: 450px;
word-break: break-word;
}
.Toastify__toast {

View File

@ -21,7 +21,7 @@ import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import {
getDatabaseDetailsPath,
getServiceDetailsPath,
getTeamDetailsPath,
getTeamAndUserDetailsPath,
} from '../constants/constants';
import { ColumnTestType } from '../enums/columnTest.enum';
import { EntityType, FqnPart } from '../enums/entity.enum';
@ -127,7 +127,7 @@ export const getEntityOverview = (
{
name: 'Owner',
value: ownerValue?.displayName || ownerValue?.name || '--',
url: getTeamDetailsPath(owner?.name || ''),
url: getTeamAndUserDetailsPath(owner?.name || ''),
isLink: ownerValue
? ownerValue.type === 'team'
? true
@ -205,7 +205,7 @@ export const getEntityOverview = (
{
name: 'Owner',
value: ownerValue?.displayName || ownerValue?.name || '--',
url: getTeamDetailsPath(owner?.name || ''),
url: getTeamAndUserDetailsPath(owner?.name || ''),
isLink: ownerValue
? ownerValue.type === 'team'
? true
@ -253,7 +253,7 @@ export const getEntityOverview = (
{
name: 'Owner',
value: ownerValue?.displayName || ownerValue?.name || '--',
url: getTeamDetailsPath(owner?.name || ''),
url: getTeamAndUserDetailsPath(owner?.name || ''),
isLink: ownerValue
? ownerValue.type === 'team'
? true

View File

@ -22,7 +22,10 @@ import { Link } from 'react-router-dom';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import { DESCRIPTIONLENGTH, getTeamDetailsPath } from '../constants/constants';
import {
DESCRIPTIONLENGTH,
getTeamAndUserDetailsPath,
} from '../constants/constants';
import { ChangeType } from '../enums/entity.enum';
import { Column } from '../generated/entity/data/table';
import {
@ -383,7 +386,7 @@ export const feedSummaryFromatter = (
{newValue?.type === 'team' ? (
<Link
className="tw-pl-1"
to={getTeamDetailsPath(newValue?.name || '')}>
to={getTeamAndUserDetailsPath(newValue?.name || '')}>
<span title={ownerName}>{ownerName}</span>
</Link>
) : (
@ -397,7 +400,7 @@ export const feedSummaryFromatter = (
{value?.type === 'team' ? (
<Link
className="tw-pl-1"
to={getTeamDetailsPath(value?.name || '')}>
to={getTeamAndUserDetailsPath(value?.name || '')}>
<span title={ownerName}>{ownerName}</span>
</Link>
) : (