Fix #4293 UI: Add support for Delete recursive=true for given entities. (#4314)

* Fix #4293 UI: Add support for Delete `recursive=true` for given entities.

* Remove tooltip from entity feed card header

* pass recursive if its not undefined.

* Chnage confirmation text to `DELETE`

* Add deleteEntity message support

* Add support for database and database schema count

* Add support for delete entity message

* Fix Faling test

* Remove unwanted code

* Add unit test

* Addressing review comment
This commit is contained in:
Sachin Chaurasiya 2022-04-21 17:44:34 +05:30 committed by GitHub
parent e4e1d4971b
commit c7c3d153e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 119 additions and 74 deletions

View File

@ -12,8 +12,10 @@
*/
import { AxiosResponse } from 'axios';
import { isUndefined } from 'lodash';
import { Edge } from '../components/EntityLineage/EntityLineage.interface';
import { SearchIndex } from '../enums/search.enum';
import { getURLWithQueryFields } from '../utils/APIUtils';
import { getCurrentUserId } from '../utils/CommonUtils';
import { getSearchAPIQuery } from '../utils/SearchUtils';
import APIClient from './index';
@ -116,7 +118,18 @@ export const getInitialEntity: Function = (): Promise<AxiosResponse> => {
export const deleteEntity: Function = (
entityType: string,
entityId: string
entityId: string,
isRecursive: boolean
): Promise<AxiosResponse> => {
return APIClient.delete(`/${entityType}/${entityId}?hardDelete=true`);
const searchParams = new URLSearchParams({ hardDelete: `true` });
if (!isUndefined(isRecursive)) {
searchParams.set('recursive', `${isRecursive}`);
}
const path = getURLWithQueryFields(
`/${entityType}/${entityId}`,
'',
`${searchParams.toString()}`
);
return APIClient.delete(path);
};

View File

@ -26,6 +26,7 @@ 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';
import { useAuth } from '../../hooks/authHooks';
import jsonData from '../../jsons/en';
@ -53,6 +54,8 @@ const ManageTab: FunctionComponent<ManageProps> = ({
entityName,
entityType,
entityId,
isRecursiveDelete,
deletEntityMessage,
}: ManageProps) => {
const history = useHistory();
const { userPermissions, isAdminUser } = useAuth();
@ -166,9 +169,28 @@ const ManageTab: FunctionComponent<ManageProps> = ({
setEntityDeleteState(ENTITY_DELETE_STATE);
};
const prepareEntityType = () => {
const services = [
EntityType.DASHBOARD_SERVICE,
EntityType.DATABASE_SERVICE,
EntityType.MESSAGING_SERVICE,
EntityType.PIPELINE_SERVICE,
];
if (services.includes((entityType || '') as EntityType)) {
return `services/${entityType}s`;
} else {
return `${entityType}s`;
}
};
const prepareDeleteMessage = () => {
return `Once you delete this ${entityType}, it will be removed permanently`;
};
const handleOnEntityDeleteConfirm = () => {
setEntityDeleteState((prev) => ({ ...prev, loading: 'waiting' }));
deleteEntity(`${entityType}s`, entityId)
deleteEntity(prepareEntityType(), entityId, isRecursiveDelete)
.then((res: AxiosResponse) => {
if (res.status === 200) {
setTimeout(() => {
@ -201,6 +223,7 @@ const ManageTab: FunctionComponent<ManageProps> = ({
if (allowDelete && entityDeleteState.state) {
return (
<EntityDeleteModal
bodyText={deletEntityMessage || prepareDeleteMessage()}
entityName={entityName as string}
entityType={entityType as string}
loadingState={entityDeleteState.loading}
@ -222,9 +245,7 @@ const ManageTab: FunctionComponent<ManageProps> = ({
<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>
<p data-testid="danger-zone-text-para">{prepareDeleteMessage()}</p>
</div>
<NonAdminAction
className="tw-self-center"

View File

@ -29,6 +29,8 @@ export interface ManageProps {
entityName?: string;
entityType?: string;
allowDelete?: boolean;
isRecursiveDelete?: boolean;
deletEntityMessage?: string;
}
export type Status = 'initial' | 'waiting' | 'success';

View File

@ -96,7 +96,7 @@ describe('Test EntityDelete Modal Component', () => {
const inputBox = await findByTestId(container, 'confirmation-text-input');
fireEvent.change(inputBox, {
target: { value: `${mockProp.entityType}/${mockProp.entityName}` },
target: { value: 'DELETE' },
});
expect(confirmButton).not.toBeDisabled();

View File

@ -28,6 +28,7 @@ interface Prop extends HTMLAttributes<HTMLDivElement> {
entityName: string;
entityType: string;
loadingState: string;
bodyText?: string;
}
const EntityDeleteModal: FC<Prop> = ({
@ -37,6 +38,7 @@ const EntityDeleteModal: FC<Prop> = ({
entityType,
onCancel,
onConfirm,
bodyText,
}: Prop) => {
const [name, setName] = useState('');
@ -45,7 +47,7 @@ const EntityDeleteModal: FC<Prop> = ({
};
const isNameMatching = useCallback(() => {
return name === `${entityType}/${entityName}`;
return name === 'DELETE';
}, [name]);
return (
@ -60,13 +62,12 @@ const EntityDeleteModal: FC<Prop> = ({
</p>
</div>
<div className={classNames('tw-modal-body')} data-testid="body-text">
<p className="tw-mb-2">{`Once you delete this ${entityType}, it would be removed permanently`}</p>
<p className="tw-mb-2">
Type{' '}
<strong>
{entityType}/{entityName}
</strong>{' '}
to confirm
{bodyText ||
`Once you delete this ${entityType}, it will be removed permanently`}
</p>
<p className="tw-mb-2">
Type <strong>DELETE</strong> to confirm
</p>
<input
autoComplete="off"

View File

@ -26,7 +26,6 @@ import { TableType } from '../../../generated/entity/data/table';
import { TagLabel } from '../../../generated/type/tagLabel';
import { serviceTypeLogo } from '../../../utils/ServiceUtils';
import { getEntityLink, getUsagePercentile } from '../../../utils/TableUtils';
import PopOver from '../popover/PopOver';
import TableDataCardBody from './TableDataCardBody';
type Props = {
@ -63,9 +62,6 @@ const TableDataCard: FunctionComponent<Props> = ({
indexType,
matches,
tableType,
service,
database,
databaseSchema,
deleted = false,
}: Props) => {
const location = useLocation();
@ -116,45 +112,6 @@ const TableDataCard: FunctionComponent<Props> = ({
}
};
const getPopOverContent = () => {
const entityDetails = [
{
key: 'Service Type',
value: serviceType,
},
];
if (service) {
entityDetails.push({
key: 'Service',
value: service,
});
}
if (database) {
entityDetails.push({
key: 'Database',
value: database,
});
}
if (databaseSchema) {
entityDetails.push({
key: 'Schema',
value: databaseSchema,
});
}
return (
<div className="tw-text-left">
{entityDetails.map((detail) => (
<p key={detail.key}>
<span className="tw-text-grey-muted">{detail.key} : </span>
<span className="tw-ml-2">{detail.value}</span>
</p>
))}
</div>
);
};
return (
<div
className="tw-bg-white tw-p-3 tw-border tw-border-main tw-rounded-md"
@ -167,21 +124,15 @@ const TableDataCard: FunctionComponent<Props> = ({
className="tw-inline tw-h-5 tw-w-5"
src={serviceTypeLogo(serviceType || '')}
/>
<PopOver
html={getPopOverContent()}
position="top"
theme="light"
trigger="mouseenter">
<h6 className="tw-flex tw-items-center tw-m-0 tw-heading tw-pl-2">
<button
className="tw-text-grey-body tw-font-medium"
data-testid="table-link"
id={`${id}Title`}
onClick={handleLinkClick}>
{fullyQualifiedName}
</button>
</h6>
</PopOver>
<h6 className="tw-flex tw-items-center tw-m-0 tw-heading tw-pl-2">
<button
className="tw-text-grey-body tw-font-medium"
data-testid="table-link"
id={`${id}Title`}
onClick={handleLinkClick}>
{fullyQualifiedName}
</button>
</h6>
{deleted && (
<>
<div

View File

@ -75,6 +75,7 @@ import {
getPartialNameFromTableFQN,
hasEditAccess,
isEven,
pluralize,
} from '../../utils/CommonUtils';
import {
databaseSchemaDetailsTabs,
@ -566,6 +567,18 @@ const DatabaseSchemaPage: FunctionComponent = () => {
);
};
const getDeleteEntityMessage = () => {
if (!tableInstanceCount) {
return;
}
return `Deleting this databaseSchema will also delete ${pluralize(
tableInstanceCount,
'table',
's'
)}.`;
};
useEffect(() => {
if (TabSpecificField.ACTIVITY_FEED === tab) {
fetchActivityFeed();
@ -675,7 +688,9 @@ const DatabaseSchemaPage: FunctionComponent = () => {
<ManageTabComponent
allowDelete
hideTier
isRecursiveDelete
currentUser={databaseSchema?.owner?.id}
deletEntityMessage={getDeleteEntityMessage()}
entityId={databaseSchema?.id}
entityName={databaseSchema?.name}
entityType={EntityType.DATABASE_SCHEMA}

View File

@ -38,6 +38,7 @@ import {
postFeedById,
postThread,
} from '../../axiosAPIs/feedsAPI';
import { getAllTables } from '../../axiosAPIs/tableAPI';
import ActivityFeedList from '../../components/ActivityFeed/ActivityFeedList/ActivityFeedList';
import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
import Description from '../../components/common/description/Description';
@ -71,7 +72,12 @@ import { EntityReference } from '../../generated/entity/teams/user';
import { Paging } from '../../generated/type/paging';
import { useInfiniteScroll } from '../../hooks/useInfiniteScroll';
import jsonData from '../../jsons/en';
import { getEntityName, hasEditAccess, isEven } from '../../utils/CommonUtils';
import {
getEntityName,
hasEditAccess,
isEven,
pluralize,
} from '../../utils/CommonUtils';
import {
databaseDetailsTabs,
getCurrentDatabaseDetailsTab,
@ -109,6 +115,7 @@ const DatabaseDetails: FunctionComponent = () => {
useState<Paging>(pagingObject);
const [databaseSchemaInstanceCount, setSchemaInstanceCount] =
useState<number>(0);
const [tableInstanceCount, setTableInstanceCount] = useState<number>(0);
const [activeTab, setActiveTab] = useState<number>(
getCurrentDatabaseDetailsTab(tab)
@ -521,6 +528,25 @@ const DatabaseDetails: FunctionComponent = () => {
});
};
const fetchTablesCount = () => {
// limit=0 will fetch empty data list with total count
getAllTables('', 0)
.then((res: AxiosResponse) => {
if (res.data) {
setTableInstanceCount(res.data.paging.total);
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['unexpected-server-response']
);
setTableInstanceCount(0);
});
};
const getLoader = () => {
return isentityThreadLoading ? <Loader /> : null;
};
@ -535,8 +561,21 @@ const DatabaseDetails: FunctionComponent = () => {
}
};
const getDeleteEntityMessage = () => {
if (!databaseSchemaInstanceCount && !tableInstanceCount) {
return;
}
return `Deleting this database will also delete ${pluralize(
databaseSchemaInstanceCount,
'schema',
's'
)} and ${pluralize(tableInstanceCount, 'table', 's')}.`;
};
useEffect(() => {
getEntityFeedCount();
fetchTablesCount();
}, []);
useEffect(() => {
@ -758,7 +797,9 @@ const DatabaseDetails: FunctionComponent = () => {
<ManageTabComponent
allowDelete
hideTier
isRecursiveDelete
currentUser={database?.owner?.id}
deletEntityMessage={getDeleteEntityMessage()}
entityId={database?.id}
entityName={database?.name}
entityType={EntityType.DATABASE}

View File

@ -975,10 +975,11 @@ const ServicePage: FunctionComponent = () => {
<ManageTabComponent
allowDelete
hideTier
isRecursiveDelete
currentUser={serviceDetails?.owner?.id}
entityId={serviceDetails?.id}
entityName={serviceDetails?.name}
entityType={`services/${serviceCategory.slice(0, -1)}`}
entityType={serviceCategory.slice(0, -1)}
hasEditAccess={hasEditAccess(
serviceDetails?.owner?.type || '',
serviceDetails?.owner?.id || ''