mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-11-03 20:19: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