mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-03 12:08:31 +00:00
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:
parent
0a07d94dfd
commit
3ceea5e425
@ -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}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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 || ''
|
||||
|
||||
@ -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 || ''
|
||||
|
||||
@ -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 || ''
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user