mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-07 22:44:08 +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 && (
|
{activeTab === 4 && !deleted && (
|
||||||
<div>
|
<div>
|
||||||
<ManageTabComponent
|
<ManageTabComponent
|
||||||
|
allowDelete
|
||||||
currentTier={tier?.tagFQN}
|
currentTier={tier?.tagFQN}
|
||||||
currentUser={owner?.id}
|
currentUser={owner?.id}
|
||||||
|
entityId={dashboardDetails.id}
|
||||||
|
entityName={dashboardDetails.name}
|
||||||
|
entityType={EntityType.DASHBOARD}
|
||||||
hasEditAccess={hasEditAccess()}
|
hasEditAccess={hasEditAccess()}
|
||||||
onSave={onSettingsUpdate}
|
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 = () => {
|
const getTierData = () => {
|
||||||
setIsLoadingTierData(true);
|
setIsLoadingTierData(true);
|
||||||
getCategory('Tier')
|
getCategory('Tier')
|
||||||
@ -225,8 +311,6 @@ const ManageTab: FunctionComponent<ManageProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const ownerName = getOwnerById();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hideTier) {
|
if (!hideTier) {
|
||||||
getTierData();
|
getTierData();
|
||||||
@ -328,55 +412,9 @@ const ManageTab: FunctionComponent<ManageProps> = ({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{!isUndefined(isJoinable) ? (
|
{getJoinableWidget()}
|
||||||
<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>
|
||||||
</div>
|
{getTierCards()}
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</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>
|
|
||||||
))}
|
|
||||||
<div className="tw-mt-6 tw-text-right" data-testid="buttons">
|
<div className="tw-mt-6 tw-text-right" data-testid="buttons">
|
||||||
<Button
|
<Button
|
||||||
size="regular"
|
size="regular"
|
||||||
@ -415,31 +453,7 @@ const ManageTab: FunctionComponent<ManageProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{allowDelete ? (
|
{getDeleteEntityWidget()}
|
||||||
<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}
|
|
||||||
{getDeleteModal()}
|
{getDeleteModal()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -153,9 +153,16 @@ describe('Test Manage tab Component', () => {
|
|||||||
expect(isJoinableSwitch).toBeInTheDocument();
|
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(
|
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');
|
const dangerZone = await findByTestId(container, 'danger-zone');
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { findByTestId, render } from '@testing-library/react';
|
import { findByTestId, fireEvent, render } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import EntityDeleteModal from './EntityDeleteModal';
|
import EntityDeleteModal from './EntityDeleteModal';
|
||||||
@ -42,11 +42,11 @@ describe('Test EntityDelete Modal Component', () => {
|
|||||||
expect(await findByTestId(container, 'body-text')).toBeInTheDocument();
|
expect(await findByTestId(container, 'body-text')).toBeInTheDocument();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await findByTestId(container, 'confirmation-text')
|
await findByTestId(container, 'confirmation-text-input')
|
||||||
).toBeInTheDocument();
|
).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} />, {
|
const { container } = render(<EntityDeleteModal {...mockProp} />, {
|
||||||
wrapper: MemoryRouter,
|
wrapper: MemoryRouter,
|
||||||
});
|
});
|
||||||
@ -56,6 +56,21 @@ describe('Test EntityDelete Modal Component', () => {
|
|||||||
expect(confirmButton).toBeDisabled();
|
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 () => {
|
it('Should show loading button if loading state is waiting', async () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<EntityDeleteModal {...mockProp} loadingState="waiting" />,
|
<EntityDeleteModal {...mockProp} loadingState="waiting" />,
|
||||||
@ -68,4 +83,38 @@ describe('Test EntityDelete Modal Component', () => {
|
|||||||
|
|
||||||
expect(loadingButton).toBeDisabled();
|
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
|
<input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="tw-form-inputs tw-px-3 tw-py-1"
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
data-testid="confirmation-text"
|
data-testid="confirmation-text-input"
|
||||||
disabled={loadingState === 'waiting'}
|
disabled={loadingState === 'waiting'}
|
||||||
name="entityName"
|
name="entityName"
|
||||||
placeholder={`${entityType}/${entityName}`}
|
placeholder={`${entityType}/${entityName}`}
|
||||||
@ -83,7 +83,7 @@ const EntityDeleteModal: FC<Prop> = ({
|
|||||||
<div className={classNames('tw-modal-footer tw-justify-end')}>
|
<div className={classNames('tw-modal-footer tw-justify-end')}>
|
||||||
<Button
|
<Button
|
||||||
className={classNames('tw-mr-2')}
|
className={classNames('tw-mr-2')}
|
||||||
data-testid="cancel"
|
data-testid="discard-button"
|
||||||
disabled={loadingState === 'waiting'}
|
disabled={loadingState === 'waiting'}
|
||||||
size="regular"
|
size="regular"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
|
|||||||
@ -574,8 +574,12 @@ const PipelineDetails = ({
|
|||||||
{activeTab === 4 && !deleted && (
|
{activeTab === 4 && !deleted && (
|
||||||
<div>
|
<div>
|
||||||
<ManageTabComponent
|
<ManageTabComponent
|
||||||
|
allowDelete
|
||||||
currentTier={tier?.tagFQN}
|
currentTier={tier?.tagFQN}
|
||||||
currentUser={owner?.id}
|
currentUser={owner?.id}
|
||||||
|
entityId={pipelineDetails.id}
|
||||||
|
entityName={pipelineDetails.name}
|
||||||
|
entityType={EntityType.PIPELINE}
|
||||||
hasEditAccess={hasEditAccess()}
|
hasEditAccess={hasEditAccess()}
|
||||||
onSave={onSettingsUpdate}
|
onSave={onSettingsUpdate}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -433,8 +433,12 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
|||||||
{activeTab === 4 && !deleted && (
|
{activeTab === 4 && !deleted && (
|
||||||
<div>
|
<div>
|
||||||
<ManageTabComponent
|
<ManageTabComponent
|
||||||
|
allowDelete
|
||||||
currentTier={tier?.tagFQN}
|
currentTier={tier?.tagFQN}
|
||||||
currentUser={owner?.id}
|
currentUser={owner?.id}
|
||||||
|
entityId={topicDetails.id}
|
||||||
|
entityName={topicDetails.name}
|
||||||
|
entityType={EntityType.TOPIC}
|
||||||
hasEditAccess={hasEditAccess()}
|
hasEditAccess={hasEditAccess()}
|
||||||
onSave={onSettingsUpdate}
|
onSave={onSettingsUpdate}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -672,8 +672,12 @@ const DatabaseSchemaPage: FunctionComponent = () => {
|
|||||||
)}
|
)}
|
||||||
{activeTab === 3 && (
|
{activeTab === 3 && (
|
||||||
<ManageTabComponent
|
<ManageTabComponent
|
||||||
|
allowDelete
|
||||||
hideTier
|
hideTier
|
||||||
currentUser={databaseSchema?.owner?.id}
|
currentUser={databaseSchema?.owner?.id}
|
||||||
|
entityId={databaseSchema?.id}
|
||||||
|
entityName={databaseSchema?.name}
|
||||||
|
entityType={EntityType.DATABASE_SCHEMA}
|
||||||
hasEditAccess={hasEditAccess(
|
hasEditAccess={hasEditAccess(
|
||||||
databaseSchema?.owner?.type || '',
|
databaseSchema?.owner?.type || '',
|
||||||
databaseSchema?.owner?.id || ''
|
databaseSchema?.owner?.id || ''
|
||||||
|
|||||||
@ -756,8 +756,12 @@ const DatabaseDetails: FunctionComponent = () => {
|
|||||||
)}
|
)}
|
||||||
{activeTab === 3 && (
|
{activeTab === 3 && (
|
||||||
<ManageTabComponent
|
<ManageTabComponent
|
||||||
|
allowDelete
|
||||||
hideTier
|
hideTier
|
||||||
currentUser={database?.owner?.id}
|
currentUser={database?.owner?.id}
|
||||||
|
entityId={database?.id}
|
||||||
|
entityName={database?.name}
|
||||||
|
entityType={EntityType.DATABASE}
|
||||||
hasEditAccess={hasEditAccess(
|
hasEditAccess={hasEditAccess(
|
||||||
database?.owner?.type || '',
|
database?.owner?.type || '',
|
||||||
database?.owner?.id || ''
|
database?.owner?.id || ''
|
||||||
|
|||||||
@ -1203,8 +1203,12 @@ const ServicePage: FunctionComponent = () => {
|
|||||||
{activeTab === 5 && (
|
{activeTab === 5 && (
|
||||||
<div className="tw-bg-white tw-h-full tw-pt-4 tw-pb-6">
|
<div className="tw-bg-white tw-h-full tw-pt-4 tw-pb-6">
|
||||||
<ManageTabComponent
|
<ManageTabComponent
|
||||||
|
allowDelete
|
||||||
hideTier
|
hideTier
|
||||||
currentUser={serviceDetails?.owner?.id}
|
currentUser={serviceDetails?.owner?.id}
|
||||||
|
entityId={serviceDetails?.id}
|
||||||
|
entityName={serviceDetails?.name}
|
||||||
|
entityType={`services/${serviceCategory.slice(0, -1)}`}
|
||||||
hasEditAccess={hasEditAccess(
|
hasEditAccess={hasEditAccess(
|
||||||
serviceDetails?.owner?.type || '',
|
serviceDetails?.owner?.type || '',
|
||||||
serviceDetails?.owner?.id || ''
|
serviceDetails?.owner?.id || ''
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user