Fix #4005 UI: Support hard deletion of entities/services with a confirmation option to make sure user is indeed hard-deleting (#4128)

This commit is contained in:
Sachin Chaurasiya 2022-04-15 10:24:06 +05:30 committed by GitHub
parent 0a07d94dfd
commit 3ceea5e425
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 176 additions and 82 deletions

View File

@ -663,8 +663,12 @@ const DashboardDetails = ({
{activeTab === 4 && !deleted && (
<div>
<ManageTabComponent
allowDelete
currentTier={tier?.tagFQN}
currentUser={owner?.id}
entityId={dashboardDetails.id}
entityName={dashboardDetails.name}
entityType={EntityType.DASHBOARD}
hasEditAccess={hasEditAccess()}
onSave={onSettingsUpdate}
/>

View File

@ -191,6 +191,92 @@ const ManageTab: FunctionComponent<ManageProps> = ({
}
};
const getDeleteEntityWidget = () => {
return allowDelete && entityId && entityName && entityType ? (
<div className="tw-mt-9" 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">
{`Once you delete this ${entityType}, it would be removed permanently`}
</p>
</div>
<Button
className="tw-px-2 tw-py-1 tw-rounded tw-h-auto tw-self-center tw-shadow"
data-testid="delete-button"
size="custom"
theme="primary"
type="button"
variant="outlined"
onClick={handleOnEntityDelete}>
Delete this {entityType}
</Button>
</div>
</div>
) : null;
};
const getTierCards = () => {
if (!hideTier) {
return isLoadingTierData ? (
<Loader />
) : (
<div className="tw-flex tw-flex-col" data-testid="cards">
{tierData.map((card, i) => (
<NonAdminAction
html={
<Fragment>
<p>You need to be owner to perform this action</p>
<p>Claim ownership from above </p>
</Fragment>
}
isOwner={hasEditAccess || Boolean(owner && !currentUser)}
key={i}
permission={Operation.UpdateTags}
position="left">
<CardListItem
card={card}
isActive={activeTier === card.id}
onSelect={handleCardSelection}
/>
</NonAdminAction>
))}
</div>
);
} else {
return null;
}
};
const getJoinableWidget = () => {
const isActionAllowed =
isAdminUser ||
isAuthDisabled ||
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" />
</div>
</div>
) : null;
return !isUndefined(isJoinable) ? (
<div className="tw-mt-3 tw-mb-1">{joinableSwitch}</div>
) : null;
};
const ownerName = getOwnerById();
const getTierData = () => {
setIsLoadingTierData(true);
getCategory('Tier')
@ -225,8 +311,6 @@ const ManageTab: FunctionComponent<ManageProps> = ({
});
};
const ownerName = getOwnerById();
useEffect(() => {
if (!hideTier) {
getTierData();
@ -328,55 +412,9 @@ const ManageTab: FunctionComponent<ManageProps> = ({
)}
</span>
</div>
{!isUndefined(isJoinable) ? (
<div className="tw-mt-3 tw-mb-1">
{isAdminUser ||
isAuthDisabled ||
userPermissions[Operation.UpdateTeam] ||
!hasEditAccess ? (
<div className="tw-flex">
<label htmlFor="join-team">Allow users to join this team</label>
<div
className={classNames(
'toggle-switch ',
teamJoinable ? 'open' : null
)}
data-testid="team-isJoinable-switch"
id="join-team"
onClick={() => setTeamJoinable((prev) => !prev)}>
<div className="switch" />
</div>
</div>
) : null}
</div>
) : null}
{getJoinableWidget()}
</div>
{!hideTier &&
(isLoadingTierData ? (
<Loader />
) : (
<div className="tw-flex tw-flex-col" data-testid="cards">
{tierData.map((card, i) => (
<NonAdminAction
html={
<>
<p>You need to be owner to perform this action</p>
<p>Claim ownership from above </p>
</>
}
isOwner={hasEditAccess || Boolean(owner && !currentUser)}
key={i}
permission={Operation.UpdateTags}
position="left">
<CardListItem
card={card}
isActive={activeTier === card.id}
onSelect={handleCardSelection}
/>
</NonAdminAction>
))}
</div>
))}
{getTierCards()}
<div className="tw-mt-6 tw-text-right" data-testid="buttons">
<Button
size="regular"
@ -415,31 +453,7 @@ const ManageTab: FunctionComponent<ManageProps> = ({
</Button>
)}
</div>
{allowDelete ? (
<div className="tw-mt-9" 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">
{`Once you delete this ${entityType}, it would be removed permanently`}
</p>
</div>
<Button
className="tw-px-2 tw-py-0 tw-rounded tw-h-8 tw-self-center tw-shadow"
data-testid="delete-button"
size="custom"
theme="primary"
type="button"
variant="outlined"
onClick={handleOnEntityDelete}>
Delete this {entityType}
</Button>
</div>
</div>
) : null}
{getDeleteEntityWidget()}
{getDeleteModal()}
</div>
);

View File

@ -153,9 +153,16 @@ describe('Test Manage tab Component', () => {
expect(isJoinableSwitch).toBeInTheDocument();
});
it('Should render danger zone if allowDelete is present', async () => {
it('Should render danger zone if allowDelete, entityId, entityName and entityType is present', async () => {
const { container } = render(
<ManageTab allowDelete hasEditAccess onSave={mockFunction} />
<ManageTab
allowDelete
hasEditAccess
entityId="testid"
entityName="testEntity"
entityType="testType"
onSave={mockFunction}
/>
);
const dangerZone = await findByTestId(container, 'danger-zone');

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { findByTestId, render } from '@testing-library/react';
import { findByTestId, fireEvent, render } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import EntityDeleteModal from './EntityDeleteModal';
@ -42,11 +42,11 @@ describe('Test EntityDelete Modal Component', () => {
expect(await findByTestId(container, 'body-text')).toBeInTheDocument();
expect(
await findByTestId(container, 'confirmation-text')
await findByTestId(container, 'confirmation-text-input')
).toBeInTheDocument();
});
it('Should initially render confirm button as disable component', async () => {
it('Should initially render confirm button as disable', async () => {
const { container } = render(<EntityDeleteModal {...mockProp} />, {
wrapper: MemoryRouter,
});
@ -56,6 +56,21 @@ describe('Test EntityDelete Modal Component', () => {
expect(confirmButton).toBeDisabled();
});
it('Should render discard button and input box as disable if loading state is wating', async () => {
const { container } = render(
<EntityDeleteModal {...mockProp} loadingState="waiting" />,
{
wrapper: MemoryRouter,
}
);
const discardButton = await findByTestId(container, 'discard-button');
const inputBox = await findByTestId(container, 'confirmation-text-input');
expect(discardButton).toBeDisabled();
expect(inputBox).toBeDisabled();
});
it('Should show loading button if loading state is waiting', async () => {
const { container } = render(
<EntityDeleteModal {...mockProp} loadingState="waiting" />,
@ -68,4 +83,38 @@ describe('Test EntityDelete Modal Component', () => {
expect(loadingButton).toBeDisabled();
});
it('Confirm button should be enable if confirm text matches', async () => {
const { container } = render(<EntityDeleteModal {...mockProp} />, {
wrapper: MemoryRouter,
});
const confirmButton = await findByTestId(container, 'confirm-button');
expect(confirmButton).toBeDisabled();
const inputBox = await findByTestId(container, 'confirmation-text-input');
fireEvent.change(inputBox, {
target: { value: `${mockProp.entityType}/${mockProp.entityName}` },
});
expect(confirmButton).not.toBeDisabled();
fireEvent.click(confirmButton);
expect(onConfirm).toHaveBeenCalled();
});
it('Should call onCancel on click of discard button', async () => {
const { container } = render(<EntityDeleteModal {...mockProp} />, {
wrapper: MemoryRouter,
});
const discardButton = await findByTestId(container, 'discard-button');
fireEvent.click(discardButton);
expect(onCancel).toHaveBeenCalled();
});
});

View File

@ -71,7 +71,7 @@ const EntityDeleteModal: FC<Prop> = ({
<input
autoComplete="off"
className="tw-form-inputs tw-px-3 tw-py-1"
data-testid="confirmation-text"
data-testid="confirmation-text-input"
disabled={loadingState === 'waiting'}
name="entityName"
placeholder={`${entityType}/${entityName}`}
@ -83,7 +83,7 @@ const EntityDeleteModal: FC<Prop> = ({
<div className={classNames('tw-modal-footer tw-justify-end')}>
<Button
className={classNames('tw-mr-2')}
data-testid="cancel"
data-testid="discard-button"
disabled={loadingState === 'waiting'}
size="regular"
theme="primary"

View File

@ -574,8 +574,12 @@ const PipelineDetails = ({
{activeTab === 4 && !deleted && (
<div>
<ManageTabComponent
allowDelete
currentTier={tier?.tagFQN}
currentUser={owner?.id}
entityId={pipelineDetails.id}
entityName={pipelineDetails.name}
entityType={EntityType.PIPELINE}
hasEditAccess={hasEditAccess()}
onSave={onSettingsUpdate}
/>

View File

@ -433,8 +433,12 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
{activeTab === 4 && !deleted && (
<div>
<ManageTabComponent
allowDelete
currentTier={tier?.tagFQN}
currentUser={owner?.id}
entityId={topicDetails.id}
entityName={topicDetails.name}
entityType={EntityType.TOPIC}
hasEditAccess={hasEditAccess()}
onSave={onSettingsUpdate}
/>

View File

@ -672,8 +672,12 @@ const DatabaseSchemaPage: FunctionComponent = () => {
)}
{activeTab === 3 && (
<ManageTabComponent
allowDelete
hideTier
currentUser={databaseSchema?.owner?.id}
entityId={databaseSchema?.id}
entityName={databaseSchema?.name}
entityType={EntityType.DATABASE_SCHEMA}
hasEditAccess={hasEditAccess(
databaseSchema?.owner?.type || '',
databaseSchema?.owner?.id || ''

View File

@ -756,8 +756,12 @@ const DatabaseDetails: FunctionComponent = () => {
)}
{activeTab === 3 && (
<ManageTabComponent
allowDelete
hideTier
currentUser={database?.owner?.id}
entityId={database?.id}
entityName={database?.name}
entityType={EntityType.DATABASE}
hasEditAccess={hasEditAccess(
database?.owner?.type || '',
database?.owner?.id || ''

View File

@ -1203,8 +1203,12 @@ const ServicePage: FunctionComponent = () => {
{activeTab === 5 && (
<div className="tw-bg-white tw-h-full tw-pt-4 tw-pb-6">
<ManageTabComponent
allowDelete
hideTier
currentUser={serviceDetails?.owner?.id}
entityId={serviceDetails?.id}
entityName={serviceDetails?.name}
entityType={`services/${serviceCategory.slice(0, -1)}`}
hasEditAccess={hasEditAccess(
serviceDetails?.owner?.type || '',
serviceDetails?.owner?.id || ''