mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-27 10:26:09 +00:00
* 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:
parent
e4e1d4971b
commit
c7c3d153e9
@ -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);
|
||||
};
|
||||
|
@ -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"
|
||||
|
@ -29,6 +29,8 @@ export interface ManageProps {
|
||||
entityName?: string;
|
||||
entityType?: string;
|
||||
allowDelete?: boolean;
|
||||
isRecursiveDelete?: boolean;
|
||||
deletEntityMessage?: string;
|
||||
}
|
||||
|
||||
export type Status = 'initial' | 'waiting' | 'success';
|
||||
|
@ -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();
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 || ''
|
||||
|
Loading…
x
Reference in New Issue
Block a user